[
  {
    "path": ".changeset/README.md",
    "content": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works\nwith multi-package repos, or single-package repos to help you version and publish your code. You can\nfind the full documentation for it [in our repository](https://github.com/changesets/changesets)\n\nWe have a quick list of common questions to get you started engaging with this project in\n[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)\n"
  },
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.1.1/schema.json\",\n  \"changelog\": \"@changesets/cli/changelog\",\n  \"commit\": false,\n  \"fixed\": [],\n  \"linked\": [],\n  \"access\": \"public\",\n  \"baseBranch\": \"main\",\n  \"updateInternalDependencies\": \"patch\",\n  \"ignore\": [\"app-router-showcase\"]\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**Reproduction**\nFor issues to be triaged in a timely manner please provide a Codesandbox/Github of the issue in it's simplest reproduction.\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n\nAny links to Google or http://schema.org/ to support to validity in terms of SEO will be a great help.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"npm\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    commit-message:\n      prefix: \"chore(deps)\"\n    open-pull-requests-limit: 5\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": ""
  },
  {
    "path": ".github/workflows/changeset-check.yml",
    "content": "name: Changeset Check\n\non:\n  pull_request:\n    types: [opened, synchronize]\n\njobs:\n  changeset-check:\n    name: Check for Changeset\n    runs-on: ubuntu-latest\n    if: github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'github-actions[bot]'\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'pnpm'\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Check for code changes\n        id: code-changes\n        run: |\n          # Get list of changed files\n          CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}..HEAD)\n          \n          # Check if any code files were changed (not just docs/config)\n          CODE_CHANGED=false\n          for file in $CHANGED_FILES; do\n            # Check if file is a code file (not docs, config, or github workflows)\n            if [[ \"$file\" =~ \\.(ts|tsx|js|jsx)$ ]] && [[ ! \"$file\" =~ ^\\.github/ ]] && [[ ! \"$file\" =~ \\.(md|mdx)$ ]]; then\n              CODE_CHANGED=true\n              break\n            fi\n          done\n          \n          echo \"code_changed=$CODE_CHANGED\" >> $GITHUB_OUTPUT\n\n      - name: Check for changeset\n        if: steps.code-changes.outputs.code_changed == 'true'\n        run: |\n          pnpm changeset status --since=origin/${{ github.base_ref }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Comment on PR if changeset is missing\n        if: failure() && steps.code-changes.outputs.code_changed == 'true'\n        uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const body = `## 📝 Changeset Required\n            \n            This PR includes code changes but is missing a changeset. Changesets help us:\n            - Track changes for release notes\n            - Determine version bumps (patch/minor/major)\n            - Credit contributors properly\n            \n            ### How to add a changeset:\n            \n            1. Run \\`pnpm changeset\\` in your local environment\n            2. Select the type of change (patch/minor/major)\n            3. Write a brief description of your changes\n            4. Commit the generated changeset file\n            \n            ### Change types:\n            - **patch**: Bug fixes, internal changes (0.0.X)\n            - **minor**: New features, non-breaking changes (0.X.0)\n            - **major**: Breaking changes (X.0.0)\n            \n            ### Example:\n            \\`\\`\\`bash\n            $ pnpm changeset\n            ? What kind of change is this? › patch\n            ? Summary › Fixed TypeScript types for ArticleJsonLd component\n            \\`\\`\\`\n            \n            If this PR only contains documentation or non-code changes, you can ignore this message.`;\n            \n            // Check if we already commented\n            const comments = await github.rest.issues.listComments({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: context.issue.number,\n            });\n            \n            const hasComment = comments.data.some(comment => \n              comment.body.includes('## 📝 Changeset Required')\n            );\n            \n            if (!hasComment) {\n              await github.rest.issues.createComment({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: context.issue.number,\n                body: body\n              });\n            }\n\n      - name: Skip message for non-code changes\n        if: steps.code-changes.outputs.code_changed == 'false'\n        run: echo \"No code changes detected, changeset not required.\""
  },
  {
    "path": ".github/workflows/changesets.yml",
    "content": "name: Changesets\n\non:\n  push:\n    branches:\n      - main\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\npermissions:\n  contents: write\n  pull-requests: write\n  id-token: write\n\njobs:\n  build:\n    name: Build Library\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Build library\n        run: pnpm build\n\n      - name: Upload build artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: dist\n          path: dist\n          retention-days: 1\n\n  lint:\n    name: Lint Library\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run ESLint\n        run: pnpm lint\n\n  typecheck:\n    name: Type Check Library\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run TypeScript type checking\n        run: pnpm typecheck\n\n  unit-tests:\n    name: Unit Tests\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run unit tests\n        run: pnpm test:unit\n\n  example-lint:\n    name: Lint Example App\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Lint example app\n        run: pnpm example:lint\n\n  example-typecheck:\n    name: Type Check Example App\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Download build artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: dist\n          path: dist\n\n      - name: Type check example app\n        run: pnpm example:typecheck\n\n  e2e-tests:\n    name: E2E Tests\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Download build artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: dist\n          path: dist\n\n      - name: Build example app\n        run: pnpm example:build\n\n      - name: Install Playwright browsers\n        run: npx playwright install --with-deps chromium\n\n      - name: Run E2E tests\n        run: pnpm test:e2e --project=chromium\n        env:\n          CI: true\n\n  release:\n    name: Create Release PR or Publish\n    runs-on: ubuntu-latest\n    needs:\n      [\n        build,\n        lint,\n        typecheck,\n        unit-tests,\n        example-lint,\n        example-typecheck,\n        e2e-tests,\n      ]\n    outputs:\n      published: ${{ steps.changesets.outputs.published }}\n      publishedPackages: ${{ steps.changesets.outputs.publishedPackages }}\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n          registry-url: \"https://registry.npmjs.org\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Download build artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: dist\n          path: dist\n\n      - name: Create Release Pull Request or Publish\n        id: changesets\n        uses: changesets/action@v1\n        with:\n          title: \"Release: Version Packages\"\n          commit: \"chore: version packages\"\n          publish: pnpm release\n          createGithubReleases: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n  post-release:\n    name: Post Release Actions\n    runs-on: ubuntu-latest\n    needs: release\n    if: needs.release.outputs.published == 'true'\n    steps:\n      - name: Report Released Packages\n        run: |\n          echo \"🎉 Released packages:\"\n          echo \"${{ needs.release.outputs.publishedPackages }}\""
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  pull_request:\n    branches: [main]\n  workflow_dispatch:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build:\n    name: Build Library\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Build library\n        run: pnpm build\n\n      - name: Upload build artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: dist\n          path: dist\n          retention-days: 1\n\n  lint:\n    name: Lint Library\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run ESLint\n        run: pnpm lint\n\n  typecheck:\n    name: Type Check Library\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run TypeScript type checking\n        run: pnpm typecheck\n\n  unit-tests:\n    name: Unit Tests\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run unit tests\n        run: pnpm test:unit\n\n      - name: Upload coverage reports\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: coverage-report\n          path: coverage/\n          retention-days: 7\n\n  example-lint:\n    name: Lint Example App\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Lint example app\n        run: pnpm example:lint\n\n  example-typecheck:\n    name: Type Check Example App\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Download build artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: dist\n          path: dist\n\n      - name: Type check example app\n        run: pnpm example:typecheck\n\n  e2e-tests:\n    name: E2E Tests\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Download build artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: dist\n          path: dist\n\n      - name: Build example app\n        run: pnpm example:build\n\n      - name: Install Playwright browsers\n        run: npx playwright install --with-deps chromium\n\n      - name: Run E2E tests\n        run: pnpm test:e2e --project=chromium\n        env:\n          CI: true\n\n      - name: Upload Playwright report\n        uses: actions/upload-artifact@v4\n        if: failure()\n        with:\n          name: playwright-report\n          path: playwright-report/\n          retention-days: 7\n\n  all-checks:\n    name: All CI Checks\n    runs-on: ubuntu-latest\n    needs:\n      [\n        build,\n        lint,\n        typecheck,\n        unit-tests,\n        example-lint,\n        example-typecheck,\n        e2e-tests,\n      ]\n    if: always()\n    steps:\n      - name: Check all job statuses\n        run: |\n          if [[ \"${{ contains(needs.*.result, 'failure') }}\" == \"true\" ]]; then\n            echo \"One or more CI checks failed\"\n            exit 1\n          elif [[ \"${{ contains(needs.*.result, 'cancelled') }}\" == \"true\" ]]; then\n            echo \"One or more CI checks were cancelled\"\n            exit 1\n          else\n            echo \"All CI checks passed successfully\"\n          fi\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Manual Release\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: \"Version to release (e.g., 7.1.0)\"\n        required: true\n        type: string\n      tag:\n        description: \"NPM tag (latest, next, alpha, beta)\"\n        required: true\n        type: choice\n        default: \"latest\"\n        options:\n          - latest\n          - next\n          - alpha\n          - beta\n\npermissions:\n  contents: write\n  id-token: write\n\njobs:\n  manual-release:\n    name: Manual Release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"pnpm\"\n          registry-url: \"https://registry.npmjs.org\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Update package version\n        run: |\n          npm version ${{ github.event.inputs.version }} --no-git-tag-version\n          echo \"Updated package.json to version ${{ github.event.inputs.version }}\"\n\n      - name: Build package\n        run: pnpm build\n\n      - name: Run TypeScript type checking\n        run: pnpm typecheck\n\n      - name: Run linting\n        run: pnpm lint\n\n      - name: Run unit tests\n        run: pnpm test:unit\n\n      - name: Lint example app\n        run: pnpm example:lint\n\n      - name: Type check example app\n        run: pnpm example:typecheck\n\n      - name: Build example app\n        run: pnpm example:build\n\n      - name: Install Playwright browsers\n        run: npx playwright install --with-deps chromium\n\n      - name: Run E2E tests\n        run: pnpm test:e2e --project=chromium\n        env:\n          CI: true\n\n      - name: Publish to npm\n        run: |\n          echo \"Publishing version ${{ github.event.inputs.version }} with tag '${{ github.event.inputs.tag }}'\"\n          pnpm publish --tag ${{ github.event.inputs.tag }} --no-git-checks\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n      - name: Create Git tag\n        run: |\n          git config user.name \"github-actions[bot]\"\n          git config user.email \"github-actions[bot]@users.noreply.github.com\"\n          git tag -a \"v${{ github.event.inputs.version }}\" -m \"Release v${{ github.event.inputs.version }}\"\n          git push origin \"v${{ github.event.inputs.version }}\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Create GitHub Release\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: \"v${{ github.event.inputs.version }}\"\n          name: \"v${{ github.event.inputs.version }}\"\n          body: |\n            ## Manual Release v${{ github.event.inputs.version }}\n\n            Published to npm with tag: `${{ github.event.inputs.tag }}`\n\n            Install with:\n            ```bash\n            npm install next-seo@${{ github.event.inputs.tag }}\n            # or\n            pnpm add next-seo@${{ github.event.inputs.tag }}\n            ```\n          prerelease: ${{ github.event.inputs.tag != 'latest' }}\n          generate_release_notes: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# Dependencies\nnode_modules\n/.pnp\n.pnp.js\n\n# Testing\ncoverage\n\n# Next.js\n.next/\nbuild/\nout/\n\n# TypeScript\n*.tsbuildinfo\n\n# Misc\n.DS_Store\n*.pem\n\n# Logs\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n*.sublime-workspace\n\n# Husky\n.husky/_/\n\n\n\n# SWC\n.swc/\n\n*.log\n.history\ndist\ncypress/videos\ncypress/screenshots\npackage-lock.json\nlib\nnotes.md\nplaywright-report\nrepomix-output.xml\n.last-run.json\n.claude\ntasks/*\ntest-results/*\n\ntask.md\nlocal_docs/*"
  },
  {
    "path": ".husky/pre-commit",
    "content": "pnpm lint-staged"
  },
  {
    "path": ".npmignore",
    "content": "# Source code\nsrc/\nexamples/\n\n# Config files\n.husky/\n.github/\n.vscode/\n.editorconfig\n.prettierignore\ncommitlint.config.js\neslint.config.mjs\nplaywright.config.ts\ntsconfig.json\ntsup.config.ts\nvitest.config.ts\npnpm-workspace.yaml\nrepomix.config.json\n\n# Documentation\n*.md\n!README.md\n\n# Test files\ntests/\ntest-results/\ncoverage/\n**/*.test.*\n**/*.spec.*\n\n# Development files\ntasks/\nschemas/\n\n# Build artifacts that shouldn't be published\n*.log\n.DS_Store"
  },
  {
    "path": ".prettierignore",
    "content": "node_modules\n.next\n.swc\ncoverage\ndist\nbuild\npackage.json\npnpm-lock.yaml\n\n# Ignore example project built files if any\nexamples/app-router-showcase/.next\nexamples/app-router-showcase/out "
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"cSpell.words\": [\"noindex\", \"nofollow\"],\n  \"typescript.tsdk\": \"node_modules/typescript/lib\"\n}\n"
  },
  {
    "path": "ADDING_NEW_COMPONENTS.md",
    "content": "# Adding New Components to Next SEO\n\nThis guide walks through the process of adding new JSON-LD structured data components to next-seo. We'll use the ArticleJsonLd component as a reference implementation.\n\n## Table of Contents\n\n1. [Research Phase](#1-research-phase)\n2. [Type Definitions](#2-type-definitions)\n3. [Component Implementation](#3-component-implementation)\n4. [Export Configuration](#4-export-configuration)\n5. [Unit Tests](#5-unit-tests)\n6. [Documentation](#6-documentation)\n7. [Example Pages](#7-example-pages)\n8. [E2E Tests](#8-e2e-tests)\n9. [Final Verification](#9-final-verification)\n\n## 1. Research Phase\n\nBefore implementing, thoroughly research the structured data specification:\n\n1. **Visit Google's Documentation**\n\n   - Go to [Google's Structured Data Gallery](https://developers.google.com/search/docs/appearance/structured-data/search-gallery)\n   - Find the specific type you're implementing (e.g., Article, Product, Recipe)\n   - Note all required and recommended properties\n\n2. **Analyze Schema Types**\n\n   - Identify all subtypes (e.g., Article has NewsArticle, BlogPosting, Blog)\n   - Note property variations between types\n   - Check for special formatting requirements (dates, images, etc.)\n\n3. **Review Existing Implementation**\n   - If updating from an older version, fetch the previous implementation\n   - Identify any missing features or properties\n   - Ensure backward compatibility where possible\n\n## 2. Type Definitions\n\nCreate comprehensive TypeScript types in `src/types/[component].types.ts`:\n\n```typescript\n// src/types/article.types.ts\nimport type { ImageObject, Person, Organization, Author } from \"./common.types\";\n\n// Note: Common types like ImageObject, Person, Organization, and Author\n// are now defined in common.types.ts to avoid duplication\n\n// Base interface with common properties\nexport interface ArticleBase {\n  headline: string;\n  url?: string;\n  author?: Author | Author[];\n  datePublished?: string;\n  dateModified?: string;\n  image?: string | ImageObject | (string | ImageObject)[];\n  publisher?: Organization;\n  description?: string;\n  isAccessibleForFree?: boolean;\n  mainEntityOfPage?:\n    | string\n    | {\n        \"@type\": \"WebPage\";\n        \"@id\": string;\n      };\n}\n\n// Specific schema type interfaces\nexport interface Article extends ArticleBase {\n  \"@type\": \"Article\";\n}\n\nexport interface NewsArticle extends ArticleBase {\n  \"@type\": \"NewsArticle\";\n}\n\nexport interface BlogPosting extends ArticleBase {\n  \"@type\": \"BlogPosting\";\n}\n\n// Component props type\nexport type ArticleJsonLdProps = (\n  | Omit<Article, \"@type\">\n  | Omit<NewsArticle, \"@type\">\n  | Omit<BlogPosting, \"@type\">\n) & {\n  type?: \"Article\" | \"NewsArticle\" | \"BlogPosting\";\n  scriptId?: string;\n  scriptKey?: string;\n};\n```\n\n### Key Patterns:\n\n- Use union types for flexible inputs (e.g., `string | Person | Organization`)\n- Support both single items and arrays where appropriate\n- Extend common interfaces to reduce duplication\n- Make all properties optional except truly required ones\n- Include component-specific props like `scriptId` and `scriptKey`\n- **Important**: Reuse types from `common.types.ts` for shared definitions like `ImageObject`, `Person`, `Organization`, and `Author`\n\n### The @type Optional Pattern\n\nA core design principle of next-seo is that developers should not need to specify `@type` properties manually. This provides better developer experience while maintaining full Schema.org compliance.\n\n#### How It Works:\n\n1. **Type Definitions**: Use `Omit<Type, \"@type\">` to create props that don't require `@type`:\n\n   ```typescript\n   export type ArticleJsonLdProps = (\n     | Omit<Article, \"@type\">\n     | Omit<NewsArticle, \"@type\">\n     | Omit<BlogPosting, \"@type\">\n   ) & {\n     type?: \"Article\" | \"NewsArticle\" | \"BlogPosting\";\n     // ... other props\n   };\n   ```\n\n2. **Process Functions**: Automatically add the correct `@type` based on input:\n\n   ```typescript\n   // Developers can pass a simple string\n   author=\"John Doe\"\n\n   // Process function converts it to a proper Person object\n   processAuthor(\"John Doe\") // → { \"@type\": \"Person\", name: \"John Doe\" }\n\n   // Or pass an object without @type\n   author={{ name: \"John Doe\", url: \"https://example.com\" }}\n\n   // Process function adds @type intelligently\n   processAuthor({...}) // → { \"@type\": \"Person\", name: \"John Doe\", url: \"...\" }\n   ```\n\n3. **Intelligent Type Detection**: Process functions use property analysis to determine types:\n   - Objects with `logo`, `address`, or `contactPoint` → Organization\n   - Objects with `familyName` or `givenName` → Person\n   - Default fallbacks ensure valid Schema.org output\n\n#### Benefits:\n\n- **Less Boilerplate**: Developers don't need to remember Schema.org type names\n- **Flexible Input**: Accept strings, objects with/without `@type`\n- **Type Safety**: Full TypeScript support throughout\n- **Forward Compatible**: Can still accept objects with `@type` if provided\n\n## 3. Component Implementation\n\nCreate the component in `src/components/[Component]JsonLd.tsx`:\n\n```typescript\n// src/components/ArticleJsonLd.tsx\nimport { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { ArticleJsonLdProps } from \"~/types/article.types\";\nimport { processAuthor, processImage } from \"~/utils/processors\";\n\n// Note: Common processing functions like processAuthor and processImage\n// are now available in ~/utils/processors.ts to avoid duplication\n\nexport default function ArticleJsonLd({\n  type = \"Article\",\n  scriptId,\n  scriptKey,\n  headline,\n  url,\n  author,\n  datePublished,\n  dateModified,\n  image,\n  publisher,\n  description,\n  isAccessibleForFree,\n  mainEntityOfPage,\n}: ArticleJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": type,\n    headline,\n    ...(url && { url }),\n    ...(author && {\n      author: Array.isArray(author)\n        ? author.map(processAuthor)\n        : processAuthor(author),\n    }),\n    ...(datePublished && { datePublished }),\n    ...(dateModified && { dateModified }),\n    // Apply defaults where appropriate\n    ...(!dateModified && datePublished && { dateModified: datePublished }),\n    ...(image && {\n      image: Array.isArray(image) ? image.map(processImage) : processImage(image),\n    }),\n    ...(publisher && { publisher }),\n    ...(description && { description }),\n    ...(isAccessibleForFree !== undefined && { isAccessibleForFree }),\n    ...(mainEntityOfPage && { mainEntityOfPage }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || `article-jsonld-${type}`}\n    />\n  );\n}\n\nexport type { ArticleJsonLdProps };\n```\n\n### Implementation Guidelines:\n\n- Use the existing `JsonLdScript` component for rendering (now with TypeScript generics support)\n- Process flexible inputs to proper schema format using shared utilities from `~/utils/processors`\n- Use object spread with conditional inclusion for optional properties\n- Handle arrays appropriately with `.map()`\n- Apply sensible defaults (e.g., dateModified defaults to datePublished)\n- Ensure boolean values are explicitly checked with `!== undefined`\n- **Always use process functions** for properties that accept flexible types (strings, objects with/without `@type`)\n- **Never require developers to specify `@type`** - the component should set the main `@type` from the `type` prop, and process functions should handle nested objects\n\n## 4. Export Configuration\n\nUpdate `src/index.ts` to export your component:\n\n```typescript\nexport { JsonLdScript } from \"./core/JsonLdScript\";\nexport {\n  default as ArticleJsonLd,\n  type ArticleJsonLdProps,\n} from \"./components/ArticleJsonLd\";\n// Add your new component here\nexport const version = \"7.0.0-alpha.0\";\n```\n\n## 5. Unit Tests\n\nCreate comprehensive tests in `src/components/[Component]JsonLd.test.tsx`:\n\n```typescript\nimport { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport ArticleJsonLd from \"./ArticleJsonLd\";\n\ndescribe(\"ArticleJsonLd\", () => {\n  it(\"renders basic Article with minimal props\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />\n    );\n\n    const script = container.querySelector('script[type=\"application/ld+json\"]');\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Article\",\n      headline: \"Test Article\",\n      datePublished: \"2024-01-01T00:00:00.000Z\",\n      dateModified: \"2024-01-01T00:00:00.000Z\", // defaults to datePublished\n    });\n  });\n\n  // Test each schema type\n  it(\"renders NewsArticle type when specified\", () => {\n    // ... test implementation\n  });\n\n  // Test flexible inputs\n  it(\"handles string author\", () => {\n    // ... converts string to Person object\n  });\n\n  it(\"handles multiple authors\", () => {\n    // ... test array handling\n  });\n\n  // Test all properties\n  it(\"handles all optional properties\", () => {\n    // ... comprehensive test with all props\n  });\n\n  // Test edge cases\n  it(\"handles isAccessibleForFree as false\", () => {\n    // ... ensure boolean false is included\n  });\n});\n```\n\n### Testing Checklist:\n\n- ✅ Basic rendering with minimal props\n- ✅ All schema type variations\n- ✅ String to object conversions\n- ✅ Array handling for authors and images\n- ✅ All optional properties\n- ✅ Default value application\n- ✅ Boolean value handling\n- ✅ Custom scriptId and scriptKey\n\n## 6. Documentation\n\nAdd comprehensive documentation to `README.md`:\n\n````markdown\n### ArticleJsonLd\n\nThe `ArticleJsonLd` component helps you add structured data for articles, blog posts, and news articles to improve their appearance in search results.\n\n#### Basic Usage\n\n```tsx\nimport { ArticleJsonLd } from \"next-seo\";\n\n<ArticleJsonLd\n  headline=\"My Amazing Article\"\n  datePublished=\"2024-01-01T08:00:00+08:00\"\n  author=\"John Doe\"\n  image=\"https://example.com/article-image.jpg\"\n  description=\"This article explains amazing things\"\n/>;\n```\n````\n\n#### Props\n\n| Property   | Type                                          | Description                                |\n| ---------- | --------------------------------------------- | ------------------------------------------ |\n| `type`     | `\"Article\" \\| \"NewsArticle\" \\| \"BlogPosting\"` | The type of article. Defaults to \"Article\" |\n| `headline` | `string`                                      | **Required.** The headline of the article  |\n| ...        | ...                                           | ...                                        |\n\n#### Best Practices\n\n1. **Always include images**: Google recommends multiple aspect ratios\n2. **Use ISO 8601 dates**: Include timezone information\n3. **Multiple authors**: List all authors when applicable\n\n````\n\n## 7. Example Pages\n\nCreate example pages in `examples/app-router-showcase/app/[component]/page.tsx`:\n\n```tsx\nimport { ArticleJsonLd } from \"next-seo\";\n\nexport default function ArticlePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ArticleJsonLd\n        headline=\"Understanding Next.js App Router\"\n        url=\"https://example.com/articles/nextjs-app-router\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        author=\"Sarah Johnson\"\n        image=\"https://example.com/images/nextjs-article.jpg\"\n        description=\"A comprehensive guide to Next.js App Router\"\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Understanding Next.js App Router</h1>\n        {/* Article content */}\n      </article>\n    </div>\n  );\n}\n````\n\nCreate examples for:\n\n- Basic usage (minimal props)\n- Advanced usage (all features)\n- Each schema type variation\n\n## 8. E2E Tests\n\nCreate Playwright tests in `tests/e2e/[component]JsonLd.e2e.spec.ts`:\n\n### Important E2E Testing Guidelines\n\n**ALL E2E tests must use real example pages!** E2E tests should test the actual component behavior through real pages in the example app. Never mock or inject content in E2E tests.\n\n❌ **DO NOT** use `page.route()` to inject mock HTML:\n\n```typescript\n// BAD - This is not a real E2E test!\nawait page.route(\"/test-page\", async (route) => {\n  await route.fulfill({\n    body: `<html>...</html>`,\n  });\n});\n```\n\n✅ **DO** create real example pages and test them:\n\n```typescript\n// GOOD - Test real pages with actual components\nawait page.goto(\"/article\");\n```\n\n### Creating E2E Tests\n\nFor every E2E test scenario, you must:\n\n1. Create a real example page in `examples/app-router-showcase/app/`\n2. Write the E2E test to navigate to that page\n3. Test the actual rendered output\n\n```typescript\nimport { test, expect } from \"@playwright/test\";\n\ntest.describe(\"ArticleJsonLd\", () => {\n  test(\"renders basic Article structured data\", async ({ page }) => {\n    // Navigate to the real example page\n    await page.goto(\"/article\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify all properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Article\");\n    expect(jsonData.headline).toBe(\"Understanding Next.js App Router\");\n    // ... test all properties\n  });\n\n  test(\"properly escapes HTML entities in content\", async ({ page }) => {\n    // Navigate to a real example page with special characters\n    await page.goto(\"/article-special-chars\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n\n    // Verify JSON is valid and content is properly escaped\n    const jsonData = JSON.parse(jsonLdScript!);\n    expect(jsonData.headline).toContain(\"Special & Characters\");\n\n    // Check that dangerous content is escaped in the raw JSON\n    expect(jsonLdScript).toContain(\"\\\\u003C/script>\");\n  });\n});\n```\n\n### When to Create Additional Example Pages\n\nCreate new example pages for:\n\n- Basic usage with minimal props\n- Advanced usage with all features\n- Each schema type variation (e.g., Article, NewsArticle, BlogPosting)\n- Special characters and HTML entities\n- Edge cases with unusual data\n- Different data combinations\n\nExample structure:\n\n```\nexamples/app-router-showcase/app/\n├── article/                    # Basic article example\n├── article-advanced/           # All features\n├── news-article/              # NewsArticle type\n├── blog-posting/              # BlogPosting type\n└── article-special-chars/     # Special characters test\n```\n\nYou should also add a valid JSON test in `tests/e2e/jsonValidation.e2e.spec.ts`\n\n### Security and Escaping Tests\n\n**DO NOT add escape/security tests to individual component E2E tests!**\n\nSecurity testing for escaping dangerous sequences (like `</script>`, HTML comments, etc.) is handled centrally in `tests/e2e/security.e2e.spec.ts`. This test file comprehensively covers:\n\n- Script tag injection prevention\n- HTML comment escaping\n- Edge cases with mixed dangerous patterns\n- Safe rendering in Next.js-like environments\n\nIndividual component E2E tests should focus on:\n\n- Component-specific functionality\n- Correct data structure output\n- Schema type variations\n- Required and optional properties\n\nThe escaping functionality is a core library feature handled by the `stringify` utility, not something each component needs to test individually.\n\n## 9. Final Verification\n\nBefore completing, run all quality checks:\n\n```bash\n# 1. Run unit tests\npnpm test:unit\n\n# 2. Type checking\npnpm typecheck\n\n# 3. Linting\npnpm lint\n\n# 4. Build the package\npnpm build\n```\n\nDeveloper will run e2e manually as they can take a long time.\n\n## Common Patterns and Best Practices\n\n### Shared Utilities\n\nThe library now provides shared utilities to avoid code duplication:\n\n1. **Common Types** (`~/types/common.types.ts`):\n\n   - `ImageObject`, `Person`, `Organization`, `Author`\n   - Base interfaces like `Thing`\n\n2. **Processing Functions** (`~/utils/processors.ts`):\n\n   - `processAuthor(author: Author): Person | Organization`\n   - `processImage(image: string | ImageObject): string | ImageObject`\n\n### Flexible Input Processing\n\nUse the shared processing functions from `~/utils/processors`:\n\n```typescript\nimport { processAuthor, processImage } from \"~/utils/processors\";\n\n// These functions handle string-to-object conversions automatically\n// and add the appropriate @type without developers needing to specify it\n```\n\n**Important**: Always create or use existing process functions for properties that can accept multiple formats. This maintains the pattern of not requiring developers to specify `@type` and ensures consistent behavior across all components.\n\n### Conditional Property Inclusion\n\nUse object spread with conditional checks:\n\n```typescript\nconst data = {\n  \"@context\": \"https://schema.org\",\n  \"@type\": type,\n  headline,\n  ...(url && { url }), // Only include if truthy\n  ...(isAccessibleForFree !== undefined && { isAccessibleForFree }), // Include false values\n};\n```\n\n### Default Values\n\nApply sensible defaults where appropriate:\n\n```typescript\n// If dateModified is not provided but datePublished is, use datePublished\n...(!dateModified && datePublished && { dateModified: datePublished }),\n```\n\n### Array Handling\n\nSupport both single items and arrays:\n\n```typescript\n...(author && {\n  author: Array.isArray(author)\n    ? author.map(processAuthor)\n    : processAuthor(author),\n}),\n```\n\n## Troubleshooting\n\n### Common Issues\n\n1. **ESLint errors about unused React import**\n\n   - Remove `import React from 'react'` - it's not needed with modern JSX transform\n\n2. **Test failures with dateModified**\n\n   - Remember that dateModified defaults to datePublished when not provided\n\n3. **Boolean properties not appearing**\n\n   - Use `!== undefined` check instead of truthy check for booleans\n\n4. **Type errors with union types**\n   - Ensure proper type guards in processing functions\n\n## Checklist for New Components\n\n- [ ] Research Google's structured data documentation\n- [ ] Create comprehensive type definitions (reuse common types from `common.types.ts`)\n- [ ] Implement component using shared utilities from `~/utils/processors`\n- [ ] Update exports in src/index.ts\n- [ ] Write unit tests covering all scenarios\n- [ ] Add documentation to README.md\n- [ ] Create example pages for each variation\n- [ ] Write E2E tests (Double check guidelines!)\n- [ ] Run all quality checks (full sweep can be done via `pnpm test:sweep`)\n- [ ] Ensure backward compatibility if updating existing component\n- [ ] Check if any new processing functions should be added to shared utilities\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nNext SEO is a plugin that makes managing SEO easier in Next.js projects. It's built with TypeScript and provides components for structured data (JSON-LD) and SEO management.\n\n## Critical Rules\n\nYou must check these after coming up with a plan\n[ ] Your plan adheres to the guide found in @ADDING_NEW_COMPONENTS.md\n[ ] Your plan adheres to the guidelines found below\n\n## Development Commands\n\n### Installation\n\n```bash\npnpm install\n```\n\n### Build & Development\n\n```bash\npnpm dev          # Watch mode with tsup\npnpm build        # Build library code\n```\n\n### Code Quality\n\n```bash\npnpm lint         # Run ESLint\npnpm lint:fix     # Fix ESLint issues\npnpm format       # Format with Prettier\npnpm typecheck    # Type checking with TypeScript\n```\n\n### Testing\n\n```bash\npnpm test         # Run typecheck + lint only\npnpm test:unit    # Run unit tests with Vitest\npnpm test:unit:watch  # Watch mode for unit tests\npnpm coverage     # Generate coverage report\n# Requires pnpm build to run first\npnpm test:e2e     # Run E2E tests with Playwright\npnpm test:e2e:ui  # Run E2E tests with UI\n```\n\n### Example App\n\n```bash\npnpm example:dev    # Run example app at localhost:3001\npnpm example:build  # Build example app\npnpm example:start  # Start example app\n```\n\n### Utilities\n\n```bash\npnpm clean        # Clean build artifacts\n```\n\n## Project Architecture\n\n### Core Structure\n\n- **`/src`** - Library source code\n  - **`/core`** - Core components like `JsonLdScript`\n  - **`/types`** - TypeScript type definitions\n  - **`/utils`** - Utility functions like `stringify`\n- **`/examples/app-router-showcase`** - Example Next.js app for testing\n- **`/tests`** - Test files\n  - **`/unit`** - Unit tests (Vitest)\n  - **`/e2e`** - E2E tests (Playwright)\n\n### Build Configuration\n\n- **tsup** - For building the library (see `tsup.config.ts`)\n- Outputs both CommonJS and ESM formats\n- Path alias: `~` maps to `./src`\n\n### Testing Setup\n\n- **Vitest** - Unit testing with React Testing Library\n- **Playwright** - E2E testing running against example app on port 3001\n- Tests use `~` alias for imports\n\n## Development Notes\n\n1. All library code is in `/src` directory\n2. The project uses pnpm workspaces with the example app\n3. When developing, the example app auto-starts on port 3001 for E2E tests\n4. Lint and format are automatically run on staged files via Husky\n5. The library exports both CommonJS and ESM formats with TypeScript definitions\n6. When adding a new component ALWAYS refer to the guide found in ADDING_NEW_COMPONENTS.md\n\n## Key Patterns\n\n### @type Optional Pattern\n\nNext SEO provides excellent developer experience by **never requiring developers to manually specify `@type` properties**. This is achieved through intelligent type definitions and process functions.\n\n#### How It Works:\n\n1. **Type Definitions**: Component props use `Omit<Type, \"@type\">` to make `@type` optional\n2. **Process Functions**: Automatically add the correct `@type` based on the input\n3. **Flexible Inputs**: Accept strings, objects with/without `@type`, and arrays\n\n#### Example:\n\n```typescript\n// Developers can write this:\n<ArticleJsonLd\n  author=\"John Doe\"  // Simple string\n  publisher={{ name: \"ACME Corp\", logo: \"logo.jpg\" }}  // No @type needed\n/>\n\n// Process functions transform it to valid Schema.org:\n{\n  author: { \"@type\": \"Person\", name: \"John Doe\" },\n  publisher: { \"@type\": \"Organization\", name: \"ACME Corp\", logo: {...} }\n}\n```\n\n#### Benefits:\n\n- **Better DX**: No need to remember Schema.org type names\n- **Less Boilerplate**: Cleaner, more readable code\n- **Type Safety**: Full TypeScript support maintained\n- **Flexibility**: Still accepts objects with `@type` if provided\n\n#### Implementation Rules:\n\n1. Always use process functions for properties accepting flexible types\n2. Never require `@type` in component props\n3. Use intelligent detection (e.g., `logo` property → Organization)\n4. Provide sensible defaults in process functions\n\nThis pattern is fundamental to the library's design and must be maintained in all components.\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# next-seo\n\n## 7.2.0\n\n### Minor Changes\n\n- 28c684e: Add `review` and `aggregateRating` props to OrganizationJsonLd component, matching the existing support in LocalBusinessJsonLd. Both are direct Schema.org Organization properties processed using shared utilities.\n\n## 7.1.0\n\n### Minor Changes\n\n- d412e2b: Add HowToJsonLd component for structured data support\n  - New `HowToJsonLd` component following Schema.org HowTo specification\n  - Support for HowToStep, HowToSection, HowToDirection, and HowToTip types\n  - HowToSupply and HowToTool for materials and equipment\n  - Duration properties (prepTime, performTime, totalTime) in ISO 8601 format\n  - estimatedCost as string or MonetaryAmount object\n  - yield as string or QuantitativeValue\n  - Video support via VideoObject\n\n## 7.0.1\n\n### Patch Changes\n\n- 1db3648: Add JSDoc comment to internal type guard function\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nNext SEO is a plugin that makes managing SEO easier in Next.js projects. It's built with TypeScript and provides components for structured data (JSON-LD) and SEO management.\n\n## Critical Rules\n\nYou must check these after coming up with a plan\n[ ] Your plan adheres to the guide found in @ADDING_NEW_COMPONENTS.md\n[ ] Your plan adheres to the guidelines found below\n\n## Development Commands\n\n### Installation\n\n```bash\npnpm install\n```\n\n### Build & Development\n\n```bash\npnpm dev          # Watch mode with tsup\npnpm build        # Build library code\n```\n\n### Code Quality\n\n```bash\npnpm lint         # Run ESLint\npnpm lint:fix     # Fix ESLint issues\npnpm format       # Format with Prettier\npnpm typecheck    # Type checking with TypeScript\n```\n\n### Testing\n\n```bash\npnpm test         # Run typecheck + lint only\npnpm test:unit    # Run unit tests with Vitest\npnpm test:unit:watch  # Watch mode for unit tests\npnpm coverage     # Generate coverage report\n# Requires pnpm build to run first\npnpm test:e2e     # Run E2E tests with Playwright\npnpm test:e2e:ui  # Run E2E tests with UI\n```\n\n### Example App\n\n```bash\npnpm example:dev    # Run example app at localhost:3001\npnpm example:build  # Build example app\npnpm example:start  # Start example app\n```\n\n### Utilities\n\n```bash\npnpm clean        # Clean build artifacts\n```\n\n## Project Architecture\n\n### Core Structure\n\n- **`/src`** - Library source code\n  - **`/core`** - Core components like `JsonLdScript`\n  - **`/types`** - TypeScript type definitions\n  - **`/utils`** - Utility functions like `stringify`\n- **`/examples/app-router-showcase`** - Example Next.js app for testing\n- **`/tests`** - Test files\n  - **`/unit`** - Unit tests (Vitest)\n  - **`/e2e`** - E2E tests (Playwright)\n\n### Build Configuration\n\n- **tsup** - For building the library (see `tsup.config.ts`)\n- Outputs both CommonJS and ESM formats\n- Path alias: `~` maps to `./src`\n\n### Testing Setup\n\n- **Vitest** - Unit testing with React Testing Library\n- **Playwright** - E2E testing running against example app on port 3001\n- Tests use `~` alias for imports\n\n## Development Notes\n\n1. All library code is in `/src` directory\n2. The project uses pnpm workspaces with the example app\n3. When developing, the example app auto-starts on port 3001 for E2E tests\n4. Lint and format are automatically run on staged files via Husky\n5. The library exports both CommonJS and ESM formats with TypeScript definitions\n6. When adding a new component ALWAYS refer to the guide found in ADDING_NEW_COMPONENTS.md\n\n## Key Patterns\n\n### @type Optional Pattern\n\nNext SEO provides excellent developer experience by **never requiring developers to manually specify `@type` properties**. This is achieved through intelligent type definitions and process functions.\n\n#### How It Works:\n\n1. **Type Definitions**: Component props use `Omit<Type, \"@type\">` to make `@type` optional\n2. **Process Functions**: Automatically add the correct `@type` based on the input\n3. **Flexible Inputs**: Accept strings, objects with/without `@type`, and arrays\n\n#### Example:\n\n```typescript\n// Developers can write this:\n<ArticleJsonLd\n  author=\"John Doe\"  // Simple string\n  publisher={{ name: \"ACME Corp\", logo: \"logo.jpg\" }}  // No @type needed\n/>\n\n// Process functions transform it to valid Schema.org:\n{\n  author: { \"@type\": \"Person\", name: \"John Doe\" },\n  publisher: { \"@type\": \"Organization\", name: \"ACME Corp\", logo: {...} }\n}\n```\n\n#### Benefits:\n\n- **Better DX**: No need to remember Schema.org type names\n- **Less Boilerplate**: Cleaner, more readable code\n- **Type Safety**: Full TypeScript support maintained\n- **Flexibility**: Still accepts objects with `@type` if provided\n\n#### Implementation Rules:\n\n1. Always use process functions for properties accepting flexible types\n2. Never require `@type` in component props\n3. Use intelligent detection (e.g., `logo` property → Organization)\n4. Provide sensible defaults in process functions\n\nThis pattern is fundamental to the library's design and must be maintained in all components.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Next SEO\n\nThank you for your interest in contributing to Next SEO! We are open to all and any contributions. This guide will help you get started.\n\nIt is critical that you look over the guidance for new components [here](ADDING_NEW_COMPONENTS.md)\n\n## 🚀 Quick Start\n\n1. Fork the repository\n2. Clone your fork: `git clone git@github.com:your-username/next-seo.git`\n3. Install dependencies: `pnpm install`\n4. Create a new branch: `git checkout -b feature/your-feature-name`\n5. Make your changes\n6. Add a changeset: `pnpm changeset`\n7. Submit a pull request\n\n## 📦 Development Setup\n\n### Prerequisites\n\n- Node.js 20+ (LTS recommended)\n- pnpm 9+ (`npm install -g pnpm`)\n\n### Installation\n\n```bash\n# Clone the repository\ngit clone git@github.com:garmeeh/next-seo.git\ncd next-seo\n\n# Install dependencies\npnpm install\n\n# Start development (watch mode)\npnpm dev\n```\n\n### Available Commands\n\n```bash\npnpm dev          # Watch mode development\npnpm build        # Build the library\npnpm test         # Run type checking and linting\npnpm test:unit    # Run unit tests\npnpm test:e2e     # Run E2E tests (requires build first)\npnpm test:sweep   # Run full test suite (CI equivalent)\npnpm lint         # Check linting\npnpm format       # Format code with Prettier\n```\n\n## 📝 Adding a Changeset\n\n**Important:** All PRs with code changes require a changeset. This helps us track changes and automatically manage releases.\n\n### What is a changeset?\n\nA changeset is a piece of information about changes made in a branch or commit. It includes:\n\n- What packages changed\n- What kind of change it was (major/minor/patch)\n- A description of the change for the changelog\n\n### How to add a changeset:\n\n1. After making your changes, run: `pnpm changeset`\n2. Select the packages affected (usually just `next-seo`)\n3. Choose the type of change:\n   - **patch**: Bug fixes, documentation, internal changes (0.0.X) **Rarely use this, generally only for security, since this is an SEO package I don't want patches to slip through for people without validating**\n   - **minor**: New features, non-breaking enhancements (0.X.0) **Most common**\n   - **major**: Breaking changes (X.0.0)\n4. Write a brief description of your changes\n5. Commit the generated changeset file\n\n### Example:\n\n```bash\n$ pnpm changeset\n🦋 Which packages would you like to include? › next-seo\n🦋 Which packages should have a major bump? › (none)\n🦋 Which packages should have a minor bump? › next-seo\n🦋 The following packages will be patch bumped:\n🦋 next-seo@minor\n🦋 Please enter a summary for this change:\n📝 Added support for RecipeJsonLd component with full Schema.org compliance\n```\n\nWe recommend never using patch unless critical security bug\n\nThis creates a markdown file in `.changeset/` that will be used to:\n\n- Update the package version\n- Generate changelog entries\n- Credit you as a contributor\n\n### When is a changeset NOT required?\n\n- Documentation-only changes (README, etc.)\n- Changes to GitHub workflows\n- Changes to development tooling that don't affect the published package\n\n## 🏗️ Project Guidelines\n\n### For AI-Assisted Development\n\nThis project leverages AI coding tools. If you're using tools like Claude or GitHub Copilot:\n\n- Refer to [CLAUDE.md](CLAUDE.md) for project-specific AI guidance\n- Refer to [ADDING_NEW_COMPONENTS.md](ADDING_NEW_COMPONENTS.md) for component development\n\n### For Large Features\n\nIf you're planning a large feature or refactor:\n\n1. Open an issue first to discuss with maintainers\n2. Provide comprehensive context in your issue/PR\n3. Break down large changes into smaller, reviewable PRs if possible\n\n## 🧪 Testing Requirements\n\nBefore submitting a PR, ensure all tests pass:\n\n```bash\n# Quick checks\npnpm test         # Type checking and linting\npnpm test:unit    # Unit tests\n\n# Full validation (what CI runs)\npnpm test:sweep   # Complete test suite\n```\n\n## 🔄 Pull Request Process\n\n1. **Fork & Clone**: Fork the repo and clone locally\n2. **Branch**: Create a feature branch from `main`\n3. **Develop**: Make your changes following our guidelines\n4. **Changeset**: Add a changeset describing your changes\n5. **Test**: Ensure all tests pass\n6. **Push**: Push to your fork\n7. **PR**: Open a pull request with a clear description\n\n### PR Guidelines\n\n- Use clear, descriptive titles\n- Reference any related issues\n- Include examples if adding new features\n- Ensure CI passes before requesting review\n\n## ❓ Questions?\n\n- Open a [Discussion](https://github.com/garmeeh/next-seo/discussions) for general questions\n- Check existing issues and PRs\n- Refer to the [README](./README.md) for usage documentation\n\n## 📄 License\n\nBy contributing, you agree that your contributions will be licensed under the same MIT License that covers this project.\n\n---\n\nThank you for contributing to Next SEO! Your efforts help make SEO easier for the Next.js community.\n"
  },
  {
    "path": "CUSTOM_COMPONENTS.md",
    "content": "# Creating Custom JSON-LD Components with Next SEO\n\nThis guide shows you how to create your own structured data components using next-seo's core utilities, maintaining the same excellent developer experience as the built-in components.\n\n## Table of Contents\n\n1. [Quick Start](#quick-start)\n2. [Core Concepts](#core-concepts)\n3. [Using Built-in Processors](#using-built-in-processors)\n4. [Creating Custom Processors](#creating-custom-processors)\n5. [Advanced Patterns](#advanced-patterns)\n6. [Best Practices](#best-practices)\n7. [Real-World Examples](#real-world-examples)\n\n## Quick Start\n\nCreate a custom JSON-LD component in just a few lines:\n\n```tsx\nimport { JsonLdScript, processors } from \"next-seo\";\n\nexport function PodcastEpisodeJsonLd({ name, author, duration, url }) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"PodcastEpisode\",\n    name,\n    ...(url && { url }),\n    ...(duration && { duration }),\n    ...(author && { author: processors.processAuthor(author) }),\n  };\n\n  return <JsonLdScript data={data} scriptKey=\"podcast-episode\" />;\n}\n\n// Usage - no @type needed!\n<PodcastEpisodeJsonLd\n  name=\"Episode 1: Getting Started\"\n  author=\"Jane Doe\" // Simple string works!\n  duration=\"PT30M\"\n  url=\"https://example.com/episode-1\"\n/>;\n```\n\n## Core Concepts\n\n### The JsonLdScript Component\n\nThe `JsonLdScript` component is the foundation for rendering structured data:\n\n```tsx\nimport { JsonLdScript } from \"next-seo\";\n\n<JsonLdScript\n  data={yourStructuredData}\n  id={optionalId} // Optional: HTML id attribute\n  scriptKey={requiredKey} // Required: React key for the script element\n/>;\n```\n\n### The @type Optional Pattern\n\nNext SEO's key principle: **developers should never need to specify @type manually**. This is achieved through intelligent processors that automatically add the correct Schema.org types.\n\n```tsx\n// Your users write this:\nauthor=\"John Doe\"\n\n// Your processor converts it to:\n{ \"@type\": \"Person\", name: \"John Doe\" }\n```\n\n### Processors\n\nProcessors are functions that transform flexible inputs into properly typed Schema.org objects:\n\n```tsx\nimport { processors } from \"next-seo\";\n\n// Use built-in processors for common types\nconst author = processors.processAuthor(\"John Doe\");\nconst image = processors.processImage({ url: \"image.jpg\", width: 800 });\nconst address = processors.processAddress(\"123 Main St\");\n```\n\n## Using Built-in Processors\n\nNext SEO provides 60+ processors for common Schema.org types:\n\n### People & Organizations\n\n```tsx\nimport { processors } from \"next-seo\";\n\n// Flexible author input\nprocessors.processAuthor(\"Jane Doe\"); // → Person\nprocessors.processAuthor({ name: \"ACME Corp\", logo: \"...\" }); // → Organization\n\n// Other people/org processors\nprocessors.processPublisher(\"Tech Publishing\");\nprocessors.processOrganizer({ name: \"Event Co\", url: \"...\" });\nprocessors.processPerformer(\"Band Name\");\n```\n\n### Media & Content\n\n```tsx\n// Images - string URL or ImageObject\nprocessors.processImage(\"https://example.com/image.jpg\");\nprocessors.processImage({ url: \"...\", width: 800, height: 600 });\n\n// Videos\nprocessors.processVideo({\n  name: \"Tutorial\",\n  uploadDate: \"2024-01-01\",\n  thumbnailUrl: \"...\",\n});\n\n// Other media processors\nprocessors.processLogo(\"logo.jpg\");\nprocessors.processScreenshot({ url: \"...\", caption: \"App screenshot\" });\n```\n\n### Locations & Places\n\n```tsx\n// Simple string becomes PostalAddress\nprocessors.processAddress(\"123 Main St, City, Country\");\n\n// Object with more details\nprocessors.processAddress({\n  streetAddress: \"123 Main St\",\n  addressLocality: \"San Francisco\",\n  addressRegion: \"CA\",\n  postalCode: \"94105\",\n  addressCountry: \"US\",\n});\n\n// Places with geo coordinates\nprocessors.processPlace({\n  name: \"Office\",\n  geo: { latitude: 37.7749, longitude: -122.4194 },\n});\n```\n\n### Commerce & Offers\n\n```tsx\n// Product offers\nprocessors.processProductOffer({\n  price: 29.99,\n  priceCurrency: \"USD\",\n  availability: \"https://schema.org/InStock\",\n});\n\n// Return policies\nprocessors.processMerchantReturnPolicy({\n  returnPolicyCategory: \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n  merchantReturnDays: 30,\n});\n```\n\n## Creating Custom Processors\n\n### Basic Custom Processor\n\nCreate processors for your specific needs:\n\n```tsx\nimport { processors } from \"next-seo\";\n\n// Custom processor for a podcast host\nfunction processHost(host: string | { name: string; bio?: string }) {\n  if (typeof host === \"string\") {\n    return {\n      \"@type\": \"Person\",\n      name: host,\n    };\n  }\n\n  // Use the generic helper for objects\n  return processors.processSchemaType(host, \"Person\");\n}\n\n// Use in your component\nexport function PodcastJsonLd({ hosts, ...props }) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"PodcastSeries\",\n    ...(hosts && {\n      host: Array.isArray(hosts) ? hosts.map(processHost) : processHost(hosts),\n    }),\n  };\n\n  return <JsonLdScript data={data} scriptKey=\"podcast\" />;\n}\n```\n\n### Advanced Custom Processor with Type Detection\n\nIntelligently determine the type based on input properties:\n\n```tsx\nfunction processCreativeWork(work: string | Record<string, any>) {\n  if (typeof work === \"string\") {\n    return {\n      \"@type\": \"CreativeWork\",\n      name: work,\n    };\n  }\n\n  // Already has @type? Return as-is\n  if (work[\"@type\"]) {\n    return work;\n  }\n\n  // Detect type based on properties\n  let type = \"CreativeWork\";\n  if (\"isbn\" in work) type = \"Book\";\n  else if (\"director\" in work) type = \"Movie\";\n  else if (\"artist\" in work) type = \"MusicRecording\";\n\n  return {\n    \"@type\": type,\n    ...work,\n  };\n}\n```\n\n## Advanced Patterns\n\n### Nested Processing\n\nProcess nested structures recursively:\n\n```tsx\nfunction processEventWithVenue(event: {\n  name: string;\n  venue?: string | { name: string; address?: string };\n  organizer?: string | { name: string };\n}) {\n  return {\n    \"@type\": \"Event\",\n    name: event.name,\n    ...(event.venue && {\n      location:\n        typeof event.venue === \"string\"\n          ? processors.processPlace(event.venue)\n          : processors.processPlace({\n              ...event.venue,\n              ...(event.venue.address && {\n                address: processors.processAddress(event.venue.address),\n              }),\n            }),\n    }),\n    ...(event.organizer && {\n      organizer: processors.processOrganizer(event.organizer),\n    }),\n  };\n}\n```\n\n### Conditional Properties\n\nInclude properties only when they have values:\n\n```tsx\nexport function CustomProductJsonLd({\n  name,\n  description,\n  price,\n  image,\n  brand,\n  reviews,\n  aggregateRating,\n  ...props\n}) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Product\",\n    name,\n    ...(description && { description }),\n    ...(price && {\n      offers: {\n        \"@type\": \"Offer\",\n        price,\n        priceCurrency: \"USD\",\n      },\n    }),\n    ...(image && {\n      image: Array.isArray(image)\n        ? image.map(processors.processImage)\n        : processors.processImage(image),\n    }),\n    ...(brand && { brand: processors.processBrand(brand) }),\n    ...(reviews && {\n      review: Array.isArray(reviews)\n        ? reviews.map(processors.processReview)\n        : processors.processReview(reviews),\n    }),\n    ...(aggregateRating && {\n      aggregateRating: processors.processAggregateRating(aggregateRating),\n    }),\n  };\n\n  return <JsonLdScript data={data} scriptKey={props.scriptKey || \"product\"} />;\n}\n```\n\n### Multiple Schema Types\n\nSupport different schema types with a type prop:\n\n```tsx\ntype ScholarlyArticleType =\n  | \"ScholarlyArticle\"\n  | \"MedicalScholarlyArticle\"\n  | \"TechArticle\";\n\nexport function ScholarlyArticleJsonLd({\n  type = \"ScholarlyArticle\",\n  headline,\n  author,\n  datePublished,\n  journal,\n  doi,\n  ...props\n}: {\n  type?: ScholarlyArticleType;\n  headline: string;\n  author: string | Array<string | { name: string }>;\n  datePublished: string;\n  journal?: string;\n  doi?: string;\n}) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": type,\n    headline,\n    datePublished,\n    ...(author && {\n      author: Array.isArray(author)\n        ? author.map(processors.processAuthor)\n        : processors.processAuthor(author),\n    }),\n    ...(journal && {\n      isPartOf: {\n        \"@type\": \"PublicationIssue\",\n        name: journal,\n      },\n    }),\n    ...(doi && { identifier: processors.processIdentifier(doi) }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      scriptKey={props.scriptKey || `article-${type}`}\n    />\n  );\n}\n```\n\n## Best Practices\n\n### 1. Always Use Processors for Flexible Types\n\n```tsx\n// ✅ Good - uses processor\nauthor: processors.processAuthor(author)\n\n// ❌ Bad - requires user to specify @type\nauthor: { \"@type\": \"Person\", ...author }\n```\n\n### 2. Handle Arrays and Single Values\n\n```tsx\n// Support both single and array inputs\n...(tags && {\n  keywords: Array.isArray(tags) ? tags.join(', ') : tags\n})\n```\n\n### 3. Apply Sensible Defaults\n\n```tsx\n// Default dateModified to datePublished if not provided\nconst data = {\n  datePublished,\n  dateModified: dateModified || datePublished,\n};\n```\n\n### 4. Use TypeScript for Better DX\n\n```tsx\ninterface ServiceJsonLdProps {\n  name: string;\n  provider?: string | Organization;\n  areaServed?: string | string[];\n  serviceType?: string;\n  scriptId?: string;\n  scriptKey?: string;\n}\n```\n\n### 5. Document Your Component\n\n```tsx\n/**\n * ServiceJsonLd - Structured data for service offerings\n *\n * @example\n * <ServiceJsonLd\n *   name=\"Web Development\"\n *   provider=\"Tech Agency\"\n *   areaServed={[\"US\", \"CA\", \"UK\"]}\n *   serviceType=\"Professional Service\"\n * />\n */\nexport function ServiceJsonLd({ ... }) { ... }\n```\n\n## Real-World Examples\n\n### 1. Podcast Series with Episodes\n\n```tsx\nimport { JsonLdScript, processors } from \"next-seo\";\n\ninterface PodcastSeriesProps {\n  name: string;\n  description?: string;\n  host?: string | Array<string | { name: string; url?: string }>;\n  episodes?: Array<{\n    name: string;\n    url?: string;\n    duration?: string;\n    datePublished?: string;\n  }>;\n  image?: string | { url: string; width?: number; height?: number };\n  scriptKey?: string;\n}\n\nexport function PodcastSeriesJsonLd({\n  name,\n  description,\n  host,\n  episodes,\n  image,\n  scriptKey = \"podcast-series\",\n}: PodcastSeriesProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"PodcastSeries\",\n    name,\n    ...(description && { description }),\n    ...(host && {\n      host: Array.isArray(host)\n        ? host.map((h) =>\n            typeof h === \"string\"\n              ? { \"@type\": \"Person\", name: h }\n              : processors.processAuthor(h),\n          )\n        : typeof host === \"string\"\n          ? { \"@type\": \"Person\", name: host }\n          : processors.processAuthor(host),\n    }),\n    ...(image && { image: processors.processImage(image) }),\n    ...(episodes && {\n      episode: episodes.map((ep, index) => ({\n        \"@type\": \"PodcastEpisode\",\n        name: ep.name,\n        position: index + 1,\n        ...(ep.url && { url: ep.url }),\n        ...(ep.duration && { duration: ep.duration }),\n        ...(ep.datePublished && { datePublished: ep.datePublished }),\n      })),\n    }),\n  };\n\n  return <JsonLdScript data={data} scriptKey={scriptKey} />;\n}\n```\n\n### 2. Real Estate Listing\n\n```tsx\nimport { JsonLdScript, processors, type ImageObject } from \"next-seo\";\n\ninterface RealEstateListingProps {\n  name: string;\n  description?: string;\n  price: number;\n  priceCurrency?: string;\n  address: string | Record<string, any>;\n  images?: Array<string | ImageObject>;\n  numberOfRooms?: number;\n  floorSize?: { value: number; unitCode: string };\n  yearBuilt?: number;\n  scriptKey?: string;\n}\n\nexport function RealEstateListingJsonLd({\n  name,\n  description,\n  price,\n  priceCurrency = \"USD\",\n  address,\n  images,\n  numberOfRooms,\n  floorSize,\n  yearBuilt,\n  scriptKey = \"real-estate\",\n}: RealEstateListingProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"RealEstateListing\",\n    name,\n    ...(description && { description }),\n    offers: {\n      \"@type\": \"Offer\",\n      price,\n      priceCurrency,\n    },\n    address: processors.processAddress(address),\n    ...(images && {\n      image: images.map(processors.processImage),\n    }),\n    ...(numberOfRooms && { numberOfRooms }),\n    ...(floorSize && {\n      floorSize: processors.processQuantitativeValue(floorSize),\n    }),\n    ...(yearBuilt && { yearBuilt }),\n  };\n\n  return <JsonLdScript data={data} scriptKey={scriptKey} />;\n}\n```\n\n### 3. Service with Pricing Tiers\n\n```tsx\nimport { JsonLdScript, processors } from \"next-seo\";\n\ninterface ServiceWithPricingProps {\n  name: string;\n  provider: string | { name: string; url?: string };\n  description?: string;\n  pricingTiers?: Array<{\n    name: string;\n    price: number | { min: number; max: number };\n    features?: string[];\n  }>;\n  areaServed?: string | string[];\n  scriptKey?: string;\n}\n\nexport function ServiceWithPricingJsonLd({\n  name,\n  provider,\n  description,\n  pricingTiers,\n  areaServed,\n  scriptKey = \"service\",\n}: ServiceWithPricingProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Service\",\n    name,\n    provider: processors.processOrganization(provider),\n    ...(description && { description }),\n    ...(pricingTiers && {\n      hasOfferCatalog: {\n        \"@type\": \"OfferCatalog\",\n        name: `${name} Pricing`,\n        itemListElement: pricingTiers.map((tier) => ({\n          \"@type\": \"Offer\",\n          name: tier.name,\n          ...(typeof tier.price === \"number\"\n            ? { price: tier.price }\n            : {\n                priceSpecification: {\n                  \"@type\": \"PriceSpecification\",\n                  minPrice: tier.price.min,\n                  maxPrice: tier.price.max,\n                  priceCurrency: \"USD\",\n                },\n              }),\n          ...(tier.features && {\n            description: tier.features.join(\", \"),\n          }),\n        })),\n      },\n    }),\n    ...(areaServed && {\n      areaServed: Array.isArray(areaServed) ? areaServed : [areaServed],\n    }),\n  };\n\n  return <JsonLdScript data={data} scriptKey={scriptKey} />;\n}\n```\n\n### 4. Educational Course with Modules\n\n```tsx\nimport { JsonLdScript, processors } from \"next-seo\";\n\ninterface CourseWithModulesProps {\n  name: string;\n  description: string;\n  provider: string | { name: string; url?: string };\n  instructor?: string | Array<string | { name: string }>;\n  modules?: Array<{\n    name: string;\n    description?: string;\n    duration?: string;\n  }>;\n  price?: number;\n  startDate?: string;\n  endDate?: string;\n  scriptKey?: string;\n}\n\nexport function CourseWithModulesJsonLd({\n  name,\n  description,\n  provider,\n  instructor,\n  modules,\n  price,\n  startDate,\n  endDate,\n  scriptKey = \"course\",\n}: CourseWithModulesProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Course\",\n    name,\n    description,\n    provider: processors.processProvider(provider),\n    ...(instructor && {\n      instructor: Array.isArray(instructor)\n        ? instructor.map(processors.processAuthor)\n        : processors.processAuthor(instructor),\n    }),\n    ...(modules && {\n      hasCourseInstance: modules.map((module, index) => ({\n        \"@type\": \"CourseInstance\",\n        name: module.name,\n        courseMode: \"online\",\n        position: index + 1,\n        ...(module.description && { description: module.description }),\n        ...(module.duration && { duration: module.duration }),\n      })),\n    }),\n    ...(price !== undefined && {\n      offers: {\n        \"@type\": \"Offer\",\n        price,\n        priceCurrency: \"USD\",\n        ...(startDate && { validFrom: startDate }),\n        ...(endDate && { validThrough: endDate }),\n      },\n    }),\n  };\n\n  return <JsonLdScript data={data} scriptKey={scriptKey} />;\n}\n```\n\n## Testing Your Components\n\n### Unit Testing\n\n```tsx\nimport { render } from \"@testing-library/react\";\nimport { ServiceJsonLd } from \"./ServiceJsonLd\";\n\ndescribe(\"ServiceJsonLd\", () => {\n  it(\"renders service with basic props\", () => {\n    const { container } = render(\n      <ServiceJsonLd name=\"Consulting Service\" provider=\"Tech Solutions Inc\" />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const data = JSON.parse(script.textContent);\n\n    expect(data[\"@type\"]).toBe(\"Service\");\n    expect(data.name).toBe(\"Consulting Service\");\n    expect(data.provider[\"@type\"]).toBe(\"Organization\");\n  });\n});\n```\n\n### Validation\n\nUse Google's Rich Results Test to validate your structured data:\n\n1. Deploy your page with the custom component\n2. Visit [Google Rich Results Test](https://search.google.com/test/rich-results)\n3. Enter your URL and check for errors\n\n## Migration Guide\n\nIf you're migrating from inline JSON-LD to next-seo custom components:\n\n### Before (Inline JSON-LD)\n\n```tsx\n<script\n  type=\"application/ld+json\"\n  dangerouslySetInnerHTML={{\n    __html: JSON.stringify({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Service\",\n      name: \"My Service\",\n      provider: {\n        \"@type\": \"Organization\",\n        name: \"My Company\",\n      },\n    }),\n  }}\n/>\n```\n\n### After (Custom Component)\n\n```tsx\nimport { JsonLdScript, processors } from \"next-seo\";\n\nexport function ServiceJsonLd({ name, provider }) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Service\",\n    name,\n    provider: processors.processOrganization(provider),\n  };\n\n  return <JsonLdScript data={data} scriptKey=\"service\" />;\n}\n\n// Usage - cleaner and type-safe!\n<ServiceJsonLd\n  name=\"My Service\"\n  provider=\"My Company\" // No @type needed!\n/>;\n```\n\n## Processor API Reference\n\nFor a complete list of available processors, see the [processors export file](./src/utils/processors.export.ts). Key processors include:\n\n- `processSchemaType(value, type)` - Generic processor for any schema type\n- `processAuthor(author)` - Person or Organization\n- `processImage(image)` - String URL or ImageObject\n- `processAddress(address)` - String or PostalAddress\n- `processPlace(place)` - String or Place with address\n- `processOffer(offer)` - Offer with price and availability\n- `processReview(review)` - Review with rating and author\n- `processAggregateRating(rating)` - Aggregate rating with count\n- And 50+ more specialized processors...\n\n## Getting Help\n\n- Check existing components in [src/components](./src/components) for patterns\n- Review [ADDING_NEW_COMPONENTS.md](./ADDING_NEW_COMPONENTS.md) for internal component development\n- Open an issue for processor requests or questions\n- Contribute new processors via PR\n\n## Summary\n\nCreating custom JSON-LD components with next-seo is simple:\n\n1. Import `JsonLdScript` and `processors`\n2. Define your component props (TypeScript recommended)\n3. Use processors for flexible input handling\n4. Apply the @type optional pattern\n5. Return JsonLdScript with your data\n\nThis approach gives you:\n\n- ✅ Type safety with TypeScript\n- ✅ Flexible input handling\n- ✅ No @type boilerplate for users\n- ✅ Consistent with next-seo patterns\n- ✅ Easy to test and maintain\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Gary Meehan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2018 Gary Meehan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "LIST.md",
    "content": "# Google Search Supported Structured Data Components\n\nThis list tracks which Google Search supported structured data types are implemented in next-seo.\n\n## Implementation Status\n\n- [x] Article\n- [~] Book actions (Upcoming Deprecation)\n- [x] Breadcrumb\n- [x] Carousel\n- [~] Course info (Upcoming Deprecation)\n- [x] Course list\n- [x] Dataset\n- [x] Discussion forum\n- [x] Education Q&A (implemented as QuizJsonLd)\n- [x] Employer aggregate rating\n- [~] Estimated salary (Upcoming Deprecation)\n- [x] Event\n- [x] Fact check\n- [x] FAQ\n- [x] Image metadata\n- [x] Job posting\n- [~] Learning video (Upcoming Deprecation)\n- [x] Local business\n- [ ] Math solver\n- [x] Movie carousel\n- [x] Organization\n- [ ] Practice problem\n- [x] Product\n- [x] Merchant Listing\n- [x] Variants\n- [x] Loyalty Program\n- [x] Merchant Return Policy\n- [x] Profile page\n- [x] Q&A\n- [x] Recipe\n- [x] Review snippet\n- [x] Software app\n- [ ] Speakable\n- [~] Special announcement (Upcoming Deprecation)\n- [x] Subscription and paywalled content\n- [x] Vacation rental\n- [~] Vehicle listing (Upcoming Deprecation)\n- [x] Video\n\n## Notes\n\n- Education Q&A is implemented as `QuizJsonLd` in next-seo\n- Some components marked with icons in the Google documentation may have special requirements or be in beta\n"
  },
  {
    "path": "README.md",
    "content": "**Outrank**\n\nGet traffic and outrank competitors with Backlinks & SEO-optimized content while you sleep! I've been keeping a close eye on this new tool and it seems to be gaining a lot of traction and delivering great results. [Try it now!](https://outrank.so/?via=next-seo)\n\n[![image](https://github.com/user-attachments/assets/14c0f4c0-aad0-4d2d-8a14-6edad232a4dc)](https://outrank.so/?via=next-seo)\n\n**Have you seen the new Next.js newsletter?**\n\n[<img alt=\"NextjsWeekly banner\" src=\"./next-js-weekly.png\">](https://dub.sh/nextjsweekly)\n\n# Next SEO\n\n![npm](https://img.shields.io/npm/dw/next-seo?style=flat-square)\n![npm version](https://img.shields.io/npm/v/next-seo?style=flat-square)\n![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square)\n![License](https://img.shields.io/npm/l/next-seo?style=flat-square)\n\nNext SEO is a plugin that makes managing your SEO easier in Next.js projects. It provides components for structured data (JSON-LD) that helps search engines understand your content better.\n\n## 📋 Table of Contents\n\n_Looking for v6 documentation? [View Here](https://github.com/garmeeh/next-seo/tree/master)_\n\n_Still using <NextSeo /> component in Pages? View docs here [/src/pages/README.md]_\n\n## 🚀 Quick Start\n\n### Installation\n\n```bash\nnpm install next-seo\n# or\nyarn add next-seo\n# or\npnpm add next-seo\n# or\nbun add next-seo\n```\n\n### Basic Usage\n\n```tsx\nimport { ArticleJsonLd } from \"next-seo\";\n\nexport default function BlogPost() {\n  return (\n    <>\n      <ArticleJsonLd\n        headline=\"Getting Started with Next SEO\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        author=\"John Doe\"\n        image=\"https://example.com/article-image.jpg\"\n        description=\"Learn how to improve your Next.js SEO\"\n      />\n      <article>\n        <h1>Getting Started with Next SEO</h1>\n        {/* Your content */}\n      </article>\n    </>\n  );\n}\n```\n\n> **Note**: For standard meta tags (`<meta>`, `<title>`), use Next.js's built-in [`generateMetadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) function.\n\n> **Pages Router Support**: If you're using Next.js Pages Router, import components from `next-seo/pages`. See the [Pages Router documentation](./src/pages/README.md) for details.\n\n## Support This Project\n\n**Feel like supporting this free plugin?**\n\nIt takes a lot of time to maintain an open source project so any small contribution is greatly appreciated.\n\nCoffee fuels coding ☕️\n\n<a href=\"https://www.buymeacoffee.com/garmeeh\" target=\"_blank\"><img src=\"https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png\" alt=\"Buy Me A Coffee\" style=\"height: 60px !important;width: 217px !important;\" ></a>\n\n## Components\n\n### ArticleJsonLd\n\nThe `ArticleJsonLd` component helps you add structured data for articles, blog posts, and news articles to improve their appearance in search results.\n\n#### Basic Usage\n\n```tsx\nimport { ArticleJsonLd } from \"next-seo\";\n\nexport default function ArticlePage() {\n  return (\n    <>\n      <ArticleJsonLd\n        headline=\"My Amazing Article\"\n        datePublished=\"2024-01-01T08:00:00+08:00\"\n        author=\"John Doe\"\n        image=\"https://example.com/article-image.jpg\"\n        description=\"This article explains amazing things about Next.js SEO\"\n      />\n      <article>\n        <h1>My Amazing Article</h1>\n        {/* Article content */}\n      </article>\n    </>\n  );\n}\n```\n\n#### Advanced Example with Multiple Authors\n\n```tsx\n<ArticleJsonLd\n  type=\"NewsArticle\"\n  headline=\"Breaking: Next SEO v7 Released\"\n  url=\"https://example.com/news/next-seo-v7\"\n  datePublished=\"2024-01-01T08:00:00+08:00\"\n  dateModified=\"2024-01-02T10:00:00+08:00\"\n  author={[\n    {\n      \"@type\": \"Person\",\n      name: \"Jane Smith\",\n      url: \"https://example.com/authors/jane\",\n    },\n    \"John Doe\", // Can mix objects and strings\n  ]}\n  image={[\n    \"https://example.com/images/16x9.jpg\",\n    \"https://example.com/images/4x3.jpg\",\n    \"https://example.com/images/1x1.jpg\",\n  ]}\n  publisher={{\n    \"@type\": \"Organization\",\n    name: \"Example News\",\n    logo: \"https://example.com/logo.png\",\n  }}\n  isAccessibleForFree={true}\n/>\n```\n\n#### Blog Posting Example\n\n```tsx\n<ArticleJsonLd\n  type=\"BlogPosting\"\n  headline=\"10 Tips for Better SEO\"\n  url=\"https://example.com/blog/seo-tips\"\n  datePublished=\"2024-01-01T08:00:00+08:00\"\n  author={{\n    \"@type\": \"Organization\",\n    name: \"SEO Experts Inc.\",\n    url: \"https://example.com\",\n  }}\n  image={{\n    \"@type\": \"ImageObject\",\n    url: \"https://example.com/blog-hero.jpg\",\n    width: 1200,\n    height: 630,\n    caption: \"SEO Tips Illustration\",\n  }}\n  description=\"Learn the top 10 tips to improve your website's SEO\"\n  mainEntityOfPage={{\n    \"@type\": \"WebPage\",\n    \"@id\": \"https://example.com/blog/seo-tips\",\n  }}\n/>\n```\n\n#### Props\n\n| Property              | Type                                                    | Description                                              |\n| --------------------- | ------------------------------------------------------- | -------------------------------------------------------- |\n| `type`                | `\"Article\" \\| \"NewsArticle\" \\| \"BlogPosting\" \\| \"Blog\"` | The type of article. Defaults to \"Article\"               |\n| `headline`            | `string`                                                | **Required.** The headline of the article                |\n| `url`                 | `string`                                                | The canonical URL of the article                         |\n| `author`              | `string \\| Person \\| Organization \\| Author[]`          | The author(s) of the article                             |\n| `datePublished`       | `string`                                                | ISO 8601 date when the article was published             |\n| `dateModified`        | `string`                                                | ISO 8601 date when the article was last modified         |\n| `image`               | `string \\| ImageObject \\| (string \\| ImageObject)[]`    | Article images. Google recommends multiple aspect ratios |\n| `publisher`           | `Organization`                                          | The publisher of the article                             |\n| `description`         | `string`                                                | A short description of the article                       |\n| `isAccessibleForFree` | `boolean`                                               | Whether the article is accessible for free               |\n| `mainEntityOfPage`    | `string \\| WebPage`                                     | Indicates the article is the primary content of the page |\n| `scriptId`            | `string`                                                | Custom ID for the script tag                             |\n| `scriptKey`           | `string`                                                | Custom key prop for React                                |\n\n#### Best Practices\n\n1. **Always include images**: Google strongly recommends including high-resolution images with multiple aspect ratios (16x9, 4x3, 1x1)\n2. **Use ISO 8601 dates**: Include timezone information for accuracy\n3. **Multiple authors**: List all authors when applicable\n4. **Publisher logo**: Include a logo for NewsArticle type\n5. **Update dateModified**: Keep this current when updating content\n\n[↑ Back to Components](#-components-by-category)\n\n### ClaimReviewJsonLd\n\nThe `ClaimReviewJsonLd` component helps you add structured data for fact-checking articles that review claims made by others. This enables a summarized version of your fact check to display in Google Search results.\n\n#### Basic Usage\n\n```tsx\nimport { ClaimReviewJsonLd } from \"next-seo\";\n\nexport default function FactCheckPage() {\n  return (\n    <>\n      <ClaimReviewJsonLd\n        claimReviewed=\"The world is flat\"\n        reviewRating={{\n          ratingValue: 1,\n          bestRating: 5,\n          worstRating: 1,\n          alternateName: \"False\",\n        }}\n        url=\"https://example.com/fact-check/flat-earth\"\n        author=\"Fact Check Team\"\n      />\n      <article>\n        <h1>Fact Check: The World is Flat</h1>\n        {/* Your fact check content */}\n      </article>\n    </>\n  );\n}\n```\n\n#### Props\n\n| Property        | Type                               | Description                                                                           |\n| --------------- | ---------------------------------- | ------------------------------------------------------------------------------------- |\n| `claimReviewed` | `string`                           | **Required.** A short summary of the claim being evaluated (keep under 75 characters) |\n| `reviewRating`  | `object`                           | **Required.** The assessment of the claim with rating value and textual rating        |\n| `url`           | `string`                           | **Required.** Link to the page hosting the full fact check article                    |\n| `author`        | `string \\| Organization \\| Person` | The publisher of the fact check article                                               |\n| `itemReviewed`  | `Claim`                            | Detailed information about the claim being reviewed                                   |\n| `scriptId`      | `string`                           | Custom ID for the script tag                                                          |\n| `scriptKey`     | `string`                           | Custom key for script identification                                                  |\n\n#### Review Rating Properties\n\n| Property        | Type     | Description                                                                                 |\n| --------------- | -------- | ------------------------------------------------------------------------------------------- |\n| `alternateName` | `string` | **Required.** The truthfulness rating as human-readable text (e.g., \"False\", \"Mostly true\") |\n| `ratingValue`   | `number` | **Required.** Numeric rating (closer to bestRating = more true)                             |\n| `bestRating`    | `number` | Best value in the rating scale (must be greater than worstRating)                           |\n| `worstRating`   | `number` | Worst value in the rating scale (minimum value of 1)                                        |\n| `name`          | `string` | Alternative to alternateName (use alternateName instead)                                    |\n\n#### Advanced Example with Claim Details\n\n```tsx\n<ClaimReviewJsonLd\n  claimReviewed=\"Climate change is not real\"\n  reviewRating={{\n    ratingValue: 1,\n    bestRating: 5,\n    worstRating: 1,\n    alternateName: \"Pants on Fire\",\n  }}\n  url=\"https://example.com/fact-check/climate-denial\"\n  author={{\n    name: \"Climate Facts Organization\",\n    url: \"https://example.com\",\n    logo: \"https://example.com/logo.jpg\",\n  }}\n  itemReviewed={{\n    author: {\n      name: \"Climate Denial Institute\",\n      sameAs: \"https://climatedenial.example.com\",\n    },\n    datePublished: \"2024-06-20\",\n    appearance: {\n      url: \"https://example.com/original-claim\",\n      headline: \"The Great Climate Hoax\",\n      datePublished: \"2024-06-22\",\n      author: \"John Doe\",\n      publisher: {\n        name: \"Denial News\",\n        logo: \"https://example.com/denial-logo.jpg\",\n      },\n    },\n  }}\n/>\n```\n\n#### Best Practices\n\n1. **Clear ratings**: Use descriptive alternateName values that clearly indicate the verdict\n2. **Claim summary**: Keep claimReviewed concise (under 75 characters) to prevent wrapping\n3. **Full context**: Include itemReviewed when possible to provide claim origin details\n4. **Consistent scale**: Use a consistent rating scale across all your fact checks\n5. **Author credibility**: Clearly identify your fact-checking organization\n\n[↑ Back to Components](#-components-by-category)\n\n### CreativeWorkJsonLd\n\nThe `CreativeWorkJsonLd` component helps you add structured data for various types of creative content, with special support for marking paywalled or subscription-based content. This enables Google to differentiate paywalled content from cloaking practices.\n\n#### Basic Usage\n\n```tsx\nimport { CreativeWorkJsonLd } from \"next-seo\";\n\nexport default function ArticlePage() {\n  return (\n    <>\n      <CreativeWorkJsonLd\n        type=\"Article\"\n        headline=\"Premium Article\"\n        datePublished=\"2024-01-01T08:00:00+08:00\"\n        author=\"John Doe\"\n        description=\"This premium article requires a subscription\"\n        isAccessibleForFree={false}\n        hasPart={{\n          isAccessibleForFree: false,\n          cssSelector: \".paywall\",\n        }}\n      />\n      <article>\n        <h1>Premium Article</h1>\n        <div className=\"non-paywall\">Free preview content here...</div>\n        <div className=\"paywall\">\n          Premium content that requires subscription...\n        </div>\n      </article>\n    </>\n  );\n}\n```\n\n#### Props\n\n| Property              | Type                                                                                          | Description                                                  |\n| --------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |\n| `type`                | `\"CreativeWork\" \\| \"Article\" \\| \"NewsArticle\" \\| \"Blog\" \\| \"BlogPosting\" \\| \"Comment\" \\| ...` | The type of creative work. Defaults to \"CreativeWork\"        |\n| `headline`            | `string`                                                                                      | The headline of the content (used for Article types)         |\n| `name`                | `string`                                                                                      | The name of the content (alternative to headline)            |\n| `url`                 | `string`                                                                                      | URL of the content                                           |\n| `author`              | `string \\| Person \\| Organization \\| Array`                                                   | Author(s) of the content                                     |\n| `datePublished`       | `string`                                                                                      | ISO 8601 publication date                                    |\n| `dateModified`        | `string`                                                                                      | ISO 8601 modification date                                   |\n| `image`               | `string \\| ImageObject \\| Array`                                                              | Image(s) associated with the content                         |\n| `publisher`           | `string \\| Organization \\| Person`                                                            | Publisher of the content                                     |\n| `description`         | `string`                                                                                      | Description of the content                                   |\n| `isAccessibleForFree` | `boolean`                                                                                     | Whether the content is free or requires payment/subscription |\n| `hasPart`             | `WebPageElement \\| WebPageElement[]`                                                          | Marks specific sections as paywalled                         |\n| `mainEntityOfPage`    | `string \\| WebPage`                                                                           | The main page for this content                               |\n| `scriptId`            | `string`                                                                                      | Custom ID for the script tag                                 |\n| `scriptKey`           | `string`                                                                                      | Custom key for script identification                         |\n\n#### WebPageElement Properties (for hasPart)\n\n| Property              | Type      | Description                                         |\n| --------------------- | --------- | --------------------------------------------------- |\n| `isAccessibleForFree` | `boolean` | **Required.** Whether this section is free (false)  |\n| `cssSelector`         | `string`  | **Required.** CSS class selector (e.g., \".paywall\") |\n\n#### Marking Paywalled Content\n\n```tsx\n<CreativeWorkJsonLd\n  type=\"NewsArticle\"\n  headline=\"Breaking News: Premium Coverage\"\n  datePublished=\"2024-01-01T08:00:00+00:00\"\n  isAccessibleForFree={false}\n  hasPart={{\n    isAccessibleForFree: false,\n    cssSelector: \".premium-content\",\n  }}\n/>\n```\n\n#### Multiple Paywalled Sections\n\n```tsx\n<CreativeWorkJsonLd\n  type=\"Article\"\n  headline=\"In-Depth Analysis\"\n  datePublished=\"2024-01-01T08:00:00+00:00\"\n  isAccessibleForFree={false}\n  hasPart={[\n    {\n      isAccessibleForFree: false,\n      cssSelector: \".section1\",\n    },\n    {\n      isAccessibleForFree: false,\n      cssSelector: \".section2\",\n    },\n  ]}\n/>\n```\n\n#### Different CreativeWork Types\n\n```tsx\n// Blog with subscription content\n<CreativeWorkJsonLd\n  type=\"Blog\"\n  name=\"Premium Tech Blog\"\n  description=\"Technology insights for subscribers\"\n  isAccessibleForFree={false}\n/>\n\n// Comment\n<CreativeWorkJsonLd\n  type=\"Comment\"\n  text=\"Great article!\"\n  author=\"Jane Smith\"\n  datePublished=\"2024-01-01T10:00:00+00:00\"\n/>\n\n// Course with provider\n<CreativeWorkJsonLd\n  type=\"Course\"\n  name=\"Advanced Programming\"\n  provider=\"Tech University\"\n  description=\"Learn advanced programming concepts\"\n  isAccessibleForFree={false}\n/>\n\n// Review with rating\n<CreativeWorkJsonLd\n  type=\"Review\"\n  name=\"Product Review\"\n  itemReviewed=\"Amazing Gadget\"\n  reviewRating={{\n    ratingValue: 4.5,\n    bestRating: 5,\n  }}\n  author=\"Tech Reviewer\"\n/>\n```\n\n#### Best Practices\n\n1. **Use specific types**: Choose the most specific CreativeWork type (Article, NewsArticle, etc.) when applicable\n2. **Mark paywalled sections**: Use `hasPart` with `cssSelector` to identify paywalled content sections\n3. **Class selectors only**: Only use class selectors (e.g., \".paywall\") for `cssSelector`, not IDs or other selectors\n4. **Consistent selectors**: Ensure your HTML classes match the `cssSelector` values exactly\n5. **Complete metadata**: Include as much metadata as possible (author, dates, images) for better search results\n\n[↑ Back to Components](#-components-by-category)\n\n### RecipeJsonLd\n\nThe `RecipeJsonLd` component helps you add structured data for recipes to improve their appearance in search results with rich snippets that can include ratings, cooking times, and images.\n\n#### Basic Usage\n\n```tsx\nimport { RecipeJsonLd } from \"next-seo\";\n\nexport default function RecipePage() {\n  return (\n    <>\n      <RecipeJsonLd\n        name=\"Simple Chocolate Chip Cookies\"\n        image=\"https://example.com/cookies.jpg\"\n        description=\"Classic chocolate chip cookies that are crispy on the outside and chewy on the inside\"\n        author=\"Baker Jane\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        prepTime=\"PT20M\"\n        cookTime=\"PT12M\"\n        totalTime=\"PT32M\"\n        recipeYield=\"24 cookies\"\n        recipeCategory=\"dessert\"\n        recipeCuisine=\"American\"\n        recipeIngredient={[\n          \"2 1/4 cups all-purpose flour\",\n          \"1 cup butter, softened\",\n          \"3/4 cup granulated sugar\",\n          \"3/4 cup packed brown sugar\",\n          \"2 large eggs\",\n          \"2 teaspoons vanilla extract\",\n          \"1 teaspoon baking soda\",\n          \"1 teaspoon salt\",\n          \"2 cups chocolate chips\",\n        ]}\n        recipeInstructions={[\n          \"Preheat oven to 375°F (190°C)\",\n          \"Mix flour, baking soda, and salt in a bowl\",\n          \"In another bowl, cream butter and sugars until fluffy\",\n          \"Beat in eggs and vanilla\",\n          \"Gradually blend in flour mixture\",\n          \"Stir in chocolate chips\",\n          \"Drop by rounded tablespoons onto ungreased cookie sheets\",\n          \"Bake for 9 to 11 minutes or until golden brown\",\n        ]}\n      />\n      <article>\n        <h1>Simple Chocolate Chip Cookies</h1>\n        {/* Recipe content */}\n      </article>\n    </>\n  );\n}\n```\n\n#### Advanced Example with Structured Instructions and Nutrition\n\n```tsx\n<RecipeJsonLd\n  name=\"Gourmet Lasagna\"\n  image={[\n    \"https://example.com/lasagna-16x9.jpg\",\n    \"https://example.com/lasagna-4x3.jpg\",\n    \"https://example.com/lasagna-1x1.jpg\",\n  ]}\n  description=\"A rich and hearty lasagna with layers of meat sauce, cheese, and pasta\"\n  author={{\n    \"@type\": \"Organization\",\n    name: \"The Italian Kitchen\",\n    url: \"https://example.com\",\n  }}\n  datePublished=\"2024-01-15T10:00:00+00:00\"\n  url=\"https://example.com/recipes/gourmet-lasagna\"\n  prepTime=\"PT45M\"\n  cookTime=\"PT1H\"\n  totalTime=\"PT1H45M\"\n  recipeYield={8}\n  recipeCategory=\"main course\"\n  recipeCuisine=\"Italian\"\n  keywords=\"lasagna, italian, pasta, cheese\"\n  recipeIngredient={[\n    \"1 pound ground beef\",\n    \"1 onion, chopped\",\n    \"2 cloves garlic, minced\",\n    \"1 can (28 oz) crushed tomatoes\",\n    \"2 cans (6 oz each) tomato paste\",\n    \"16 oz ricotta cheese\",\n    \"1 egg\",\n    \"12 lasagna noodles\",\n    \"16 oz mozzarella cheese, shredded\",\n  ]}\n  recipeInstructions={[\n    {\n      \"@type\": \"HowToStep\",\n      name: \"Prepare the meat sauce\",\n      text: \"Brown ground beef with onion and garlic. Add tomatoes and tomato paste. Simmer for 30 minutes.\",\n    },\n    {\n      \"@type\": \"HowToStep\",\n      name: \"Prepare cheese mixture\",\n      text: \"Mix ricotta cheese with egg and half of the mozzarella\",\n    },\n    {\n      \"@type\": \"HowToStep\",\n      name: \"Assemble lasagna\",\n      text: \"Layer meat sauce, noodles, and cheese mixture in a 9x13 pan. Repeat layers.\",\n    },\n    {\n      \"@type\": \"HowToStep\",\n      name: \"Bake\",\n      text: \"Cover with foil and bake at 375°F for 45 minutes. Remove foil, add remaining mozzarella, and bake 15 more minutes.\",\n    },\n  ]}\n  nutrition={{\n    \"@type\": \"NutritionInformation\",\n    calories: \"450 calories\",\n    proteinContent: \"28g\",\n    carbohydrateContent: \"35g\",\n    fatContent: \"22g\",\n    saturatedFatContent: \"10g\",\n    sodiumContent: \"680mg\",\n    fiberContent: \"3g\",\n    servingSize: \"1 piece (1/8 of recipe)\",\n  }}\n  aggregateRating={{\n    \"@type\": \"AggregateRating\",\n    ratingValue: 4.7,\n    ratingCount: 234,\n    reviewCount: 189,\n  }}\n  video={{\n    \"@type\": \"VideoObject\",\n    name: \"How to Make Gourmet Lasagna\",\n    description: \"Watch our chef prepare this delicious lasagna step by step\",\n    thumbnailUrl: \"https://example.com/lasagna-video-thumb.jpg\",\n    contentUrl: \"https://example.com/videos/lasagna-tutorial.mp4\",\n    embedUrl: \"https://example.com/embed/lasagna-tutorial\",\n    uploadDate: \"2024-01-10T08:00:00+00:00\",\n    duration: \"PT8M30S\",\n  }}\n/>\n```\n\n#### Props\n\n| Property             | Type                                                                             | Description                                                                                   |\n| -------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |\n| `name`               | `string`                                                                         | **Required.** The name of the dish                                                            |\n| `image`              | `string \\| ImageObject \\| (string \\| ImageObject)[]`                             | **Required.** Images of the completed dish. Google recommends multiple high-resolution images |\n| `description`        | `string`                                                                         | A short summary describing the dish                                                           |\n| `author`             | `string \\| Person \\| Organization`                                               | The creator of the recipe                                                                     |\n| `datePublished`      | `string`                                                                         | ISO 8601 date when the recipe was published                                                   |\n| `url`                | `string`                                                                         | The canonical URL of the recipe page                                                          |\n| `prepTime`           | `string`                                                                         | ISO 8601 duration for preparation time (e.g., \"PT30M\" for 30 minutes)                         |\n| `cookTime`           | `string`                                                                         | ISO 8601 duration for cooking time                                                            |\n| `totalTime`          | `string`                                                                         | ISO 8601 duration for total time (prep + cook)                                                |\n| `recipeYield`        | `string \\| number`                                                               | The quantity produced (e.g., \"4 servings\", \"1 loaf\", or just 6)                               |\n| `recipeCategory`     | `string`                                                                         | The type of meal or course (e.g., \"dessert\", \"main course\")                                   |\n| `recipeCuisine`      | `string`                                                                         | The cuisine of the recipe (e.g., \"French\", \"Mexican\")                                         |\n| `recipeIngredient`   | `string[]`                                                                       | List of ingredients with quantities                                                           |\n| `recipeInstructions` | `string \\| HowToStep \\| HowToSection \\| (string \\| HowToStep \\| HowToSection)[]` | Step-by-step instructions                                                                     |\n| `nutrition`          | `NutritionInformation`                                                           | Nutritional information per serving                                                           |\n| `aggregateRating`    | `AggregateRating`                                                                | The aggregate rating from users                                                               |\n| `video`              | `VideoObject`                                                                    | A video showing how to make the recipe                                                        |\n| `keywords`           | `string`                                                                         | Keywords about the recipe, separated by commas                                                |\n| `scriptId`           | `string`                                                                         | Custom ID for the script tag                                                                  |\n| `scriptKey`          | `string`                                                                         | Custom key prop for React                                                                     |\n\n#### Duration Format (ISO 8601)\n\nUse these formats for time durations:\n\n- `PT15M` - 15 minutes\n- `PT1H` - 1 hour\n- `PT1H30M` - 1 hour 30 minutes\n- `PT2H15M` - 2 hours 15 minutes\n\n#### Best Practices\n\n1. **High-quality images**: Include multiple high-resolution images (16x9, 4x3, 1x1 aspect ratios)\n2. **Detailed instructions**: Use HowToStep objects for better structured data\n3. **Complete nutrition info**: Include nutrition data when possible for better search visibility\n4. **Accurate times**: Always provide prepTime and cookTime together\n5. **Ratings**: Include aggregateRating when you have user reviews\n6. **Video content**: Adding a video significantly improves search appearance\n\n[↑ Back to Components](#-components-by-category)\n\n### HowToJsonLd\n\nThe `HowToJsonLd` component helps you add structured data for how-to guides and tutorials. This can help your content appear as rich results with step-by-step instructions in search results.\n\n#### Basic Usage\n\n```tsx\nimport { HowToJsonLd } from \"next-seo\";\n\nexport default function HowToPage() {\n  return (\n    <>\n      <HowToJsonLd\n        name=\"How to Change a Flat Tire\"\n        description=\"Step-by-step instructions for safely changing a flat tire\"\n        image=\"https://example.com/tire-change.jpg\"\n        totalTime=\"PT30M\"\n        estimatedCost=\"$20\"\n        supply={[\"Spare tire\", \"Wheel wedges\"]}\n        tool={[\"Lug wrench\", \"Jack\"]}\n        step={[\n          \"Turn on hazard lights and apply wheel wedges\",\n          \"Remove the hubcap and loosen lug nuts\",\n          \"Position jack and raise the vehicle\",\n          \"Remove flat tire and mount spare\",\n          \"Lower vehicle and tighten lug nuts\",\n        ]}\n      />\n      <article>\n        <h1>How to Change a Flat Tire</h1>\n        {/* Guide content */}\n      </article>\n    </>\n  );\n}\n```\n\n#### Advanced Example with Sections and Detailed Steps\n\n```tsx\n<HowToJsonLd\n  name=\"How to Change a Flat Tire\"\n  description=\"Complete guide to safely changing a flat tire on the roadside\"\n  image={{\n    url: \"https://example.com/tire-change-guide.jpg\",\n    width: 1200,\n    height: 800,\n  }}\n  estimatedCost={{\n    currency: \"USD\",\n    value: 20,\n  }}\n  prepTime=\"PT5M\"\n  performTime=\"PT25M\"\n  totalTime=\"PT30M\"\n  yield=\"1 changed tire\"\n  tool={[\n    {\n      name: \"Spare tire\",\n    },\n    {\n      name: \"Lug wrench\",\n      image: \"https://example.com/lug-wrench.jpg\",\n    },\n    {\n      name: \"Jack\",\n    },\n    {\n      name: \"Wheel wedges\",\n      image: \"https://example.com/wheel-wedges.jpg\",\n    },\n  ]}\n  supply={[\n    {\n      name: \"Flares\",\n      image: \"https://example.com/flares.jpg\",\n    },\n  ]}\n  step={[\n    {\n      \"@type\": \"HowToSection\",\n      name: \"Preparation\",\n      position: 1,\n      itemListElement: [\n        {\n          \"@type\": \"HowToStep\",\n          position: 1,\n          itemListElement: [\n            {\n              \"@type\": \"HowToDirection\",\n              position: 1,\n              text: \"Turn on your hazard lights and set the flares.\",\n            },\n            {\n              \"@type\": \"HowToTip\",\n              position: 2,\n              text: \"You're going to need space and want to be visible.\",\n            },\n          ],\n        },\n        {\n          \"@type\": \"HowToStep\",\n          position: 2,\n          itemListElement: [\n            {\n              \"@type\": \"HowToDirection\",\n              position: 1,\n              text: \"Position wheel wedges in front of front tires if rear tire is flat, or behind rear tires if front tire is flat.\",\n            },\n            {\n              \"@type\": \"HowToTip\",\n              position: 2,\n              text: \"You don't want the car to move while you're working on it.\",\n            },\n          ],\n        },\n      ],\n    },\n    {\n      \"@type\": \"HowToSection\",\n      name: \"Raise the Car\",\n      position: 2,\n      itemListElement: [\n        {\n          \"@type\": \"HowToStep\",\n          position: 1,\n          text: \"Position the jack underneath the car, next to the flat tire.\",\n          image: \"https://example.com/position-jack.jpg\",\n        },\n        {\n          \"@type\": \"HowToStep\",\n          position: 2,\n          text: \"Raise the jack until the flat tire is just barely off of the ground.\",\n        },\n        {\n          \"@type\": \"HowToStep\",\n          position: 3,\n          text: \"Remove the hubcap and loosen the lug nuts.\",\n        },\n      ],\n    },\n    {\n      \"@type\": \"HowToSection\",\n      name: \"Finishing Up\",\n      position: 3,\n      itemListElement: [\n        {\n          \"@type\": \"HowToStep\",\n          position: 1,\n          text: \"Lower the jack and tighten the lug nuts with the wrench.\",\n        },\n        {\n          \"@type\": \"HowToStep\",\n          position: 2,\n          text: \"Replace the hubcap.\",\n        },\n        {\n          \"@type\": \"HowToStep\",\n          position: 3,\n          text: \"Put the equipment and the flat tire away.\",\n        },\n      ],\n    },\n  ]}\n  video={{\n    name: \"How to Change a Tire Video Tutorial\",\n    description: \"Watch our mechanic demonstrate the proper technique\",\n    thumbnailUrl: \"https://example.com/video-thumb.jpg\",\n    contentUrl: \"https://example.com/tire-change-video.mp4\",\n    uploadDate: \"2024-01-15T08:00:00+00:00\",\n    duration: \"PT8M\",\n  }}\n/>\n```\n\n#### Props\n\n| Property        | Type                                                 | Description                                                               |\n| --------------- | ---------------------------------------------------- | ------------------------------------------------------------------------- |\n| `name`          | `string`                                             | **Required.** The title of the how-to guide                               |\n| `description`   | `string`                                             | A brief description of the guide                                          |\n| `image`         | `string \\| ImageObject`                              | An image of the completed task or project                                 |\n| `estimatedCost` | `string \\| MonetaryAmount`                           | The estimated cost of supplies (e.g., \"$20\" or MonetaryAmount object)     |\n| `prepTime`      | `string`                                             | ISO 8601 duration for preparation time                                    |\n| `performTime`   | `string`                                             | ISO 8601 duration for the time to perform the instructions                |\n| `totalTime`     | `string`                                             | ISO 8601 duration for total time (prep + perform)                         |\n| `yield`         | `string \\| QuantitativeValue`                        | The result of performing the instructions (e.g., \"1 birdhouse\")           |\n| `supply`        | `string \\| HowToSupply \\| (string \\| HowToSupply)[]` | Supplies consumed when performing the task                                |\n| `tool`          | `string \\| HowToTool \\| (string \\| HowToTool)[]`     | Tools used but not consumed                                               |\n| `step`          | `string \\| HowToStep \\| HowToSection \\| (Step)[]`    | The steps to complete the task. Can be simple strings, steps, or sections |\n| `video`         | `VideoObject`                                        | A video showing how to complete the task                                  |\n| `scriptId`      | `string`                                             | Custom ID for the script tag                                              |\n| `scriptKey`     | `string`                                             | Custom key prop for React                                                 |\n\n#### Step Types\n\n**HowToStep** - A single step in the guide:\n\n```tsx\n{\n  \"@type\": \"HowToStep\",\n  name: \"Step Name\",           // Optional step title\n  text: \"Step instructions\",   // The instruction text\n  url: \"https://...\",         // Optional URL for more details\n  image: \"https://...\",       // Optional step image\n}\n```\n\n**HowToSection** - A group of related steps:\n\n```tsx\n{\n  \"@type\": \"HowToSection\",\n  name: \"Section Name\",\n  position: 1,\n  itemListElement: [\n    { \"@type\": \"HowToStep\", text: \"First step\" },\n    { \"@type\": \"HowToStep\", text: \"Second step\" },\n  ]\n}\n```\n\n**HowToDirection** and **HowToTip** - For detailed step content:\n\n```tsx\n{\n  \"@type\": \"HowToStep\",\n  itemListElement: [\n    {\n      \"@type\": \"HowToDirection\",\n      text: \"Do this specific action\",\n      beforeMedia: \"https://example.com/before.jpg\",\n      afterMedia: \"https://example.com/after.jpg\",\n    },\n    {\n      \"@type\": \"HowToTip\",\n      text: \"Here's a helpful tip\",\n    }\n  ]\n}\n```\n\n#### Duration Format (ISO 8601)\n\nUse these formats for time durations:\n\n- `PT15M` - 15 minutes\n- `PT1H` - 1 hour\n- `PT1H30M` - 1 hour 30 minutes\n- `PT2H15M` - 2 hours 15 minutes\n\n#### Best Practices\n\n1. **Clear steps**: Write concise, actionable step instructions\n2. **Include images**: Add images for complex steps to improve clarity\n3. **Separate sections**: Use HowToSection to group related steps logically\n4. **Accurate timing**: Provide realistic time estimates for each phase\n5. **List all materials**: Include all supplies and tools needed upfront\n6. **Add video**: Video content significantly improves search appearance\n\n[↑ Back to Components](#-components-by-category)\n\n### OrganizationJsonLd\n\nThe `OrganizationJsonLd` component helps you add structured data about your organization to improve how it appears in search results and knowledge panels.\n\n#### Basic Usage\n\n```tsx\nimport { OrganizationJsonLd } from \"next-seo\";\n\nexport default function AboutPage() {\n  return (\n    <>\n      <OrganizationJsonLd\n        name=\"Example Corporation\"\n        url=\"https://www.example.com\"\n        logo=\"https://www.example.com/logo.png\"\n        description=\"The example corporation is well-known for producing high-quality widgets\"\n        sameAs={[\n          \"https://twitter.com/example\",\n          \"https://facebook.com/example\",\n          \"https://linkedin.com/company/example\",\n        ]}\n      />\n      <div>\n        <h1>About Example Corporation</h1>\n        {/* About page content */}\n      </div>\n    </>\n  );\n}\n```\n\n#### Advanced Example with Address and Contact\n\n```tsx\n<OrganizationJsonLd\n  type=\"Organization\"\n  name=\"Example Corporation\"\n  url=\"https://www.example.com\"\n  logo={{\n    \"@type\": \"ImageObject\",\n    url: \"https://www.example.com/logo.png\",\n    width: 600,\n    height: 400,\n  }}\n  description=\"Leading provider of innovative widget solutions\"\n  sameAs={[\n    \"https://example.net/profile/example1234\",\n    \"https://example.org/example1234\",\n  ]}\n  address={{\n    \"@type\": \"PostalAddress\",\n    streetAddress: \"999 W Example St Suite 99\",\n    addressLocality: \"New York\",\n    addressRegion: \"NY\",\n    postalCode: \"10019\",\n    addressCountry: \"US\",\n  }}\n  contactPoint={{\n    \"@type\": \"ContactPoint\",\n    contactType: \"Customer Service\",\n    telephone: \"+1-999-999-9999\",\n    email: \"support@example.com\",\n  }}\n  telephone=\"+1-999-999-9999\"\n  email=\"contact@example.com\"\n  foundingDate=\"2010-01-01\"\n  vatID=\"FR12345678901\"\n  iso6523Code=\"0199:724500PMK2A2M1SQQ228\"\n  numberOfEmployees={{\n    minValue: 100,\n    maxValue: 999,\n  }}\n/>\n```\n\n#### OnlineStore Example with Return Policy\n\n```tsx\n<OrganizationJsonLd\n  type=\"OnlineStore\"\n  name=\"Example Online Store\"\n  url=\"https://www.example.com\"\n  logo=\"https://www.example.com/assets/logo.png\"\n  contactPoint={{\n    \"@type\": \"ContactPoint\",\n    contactType: \"Customer Service\",\n    email: \"support@example.com\",\n    telephone: \"+47-99-999-9900\",\n  }}\n  vatID=\"FR12345678901\"\n  iso6523Code=\"0199:724500PMK2A2M1SQQ228\"\n  hasMerchantReturnPolicy={{\n    \"@type\": \"MerchantReturnPolicy\",\n    applicableCountry: [\"FR\", \"CH\"],\n    returnPolicyCountry: \"FR\",\n    returnPolicyCategory: \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n    merchantReturnDays: 60,\n    returnMethod: \"https://schema.org/ReturnByMail\",\n    returnFees: \"https://schema.org/FreeReturn\",\n    refundType: \"https://schema.org/FullRefund\",\n  }}\n/>\n```\n\n#### Props\n\n| Property                  | Type                                                     | Description                                             |\n| ------------------------- | -------------------------------------------------------- | ------------------------------------------------------- |\n| `type`                    | `\"Organization\" \\| \"OnlineStore\"`                        | The type of organization. Defaults to \"Organization\"    |\n| `name`                    | `string`                                                 | The name of your organization                           |\n| `url`                     | `string`                                                 | The URL of your organization's website                  |\n| `logo`                    | `string \\| ImageObject`                                  | Your organization's logo (112x112px minimum)            |\n| `description`             | `string`                                                 | A detailed description of your organization             |\n| `sameAs`                  | `string \\| string[]`                                     | URLs of your organization's profiles on other sites     |\n| `address`                 | `string \\| PostalAddress \\| (string \\| PostalAddress)[]` | Physical or mailing address(es)                         |\n| `contactPoint`            | `ContactPoint \\| ContactPoint[]`                         | Contact information for your organization               |\n| `telephone`               | `string`                                                 | Primary phone number (include country code)             |\n| `email`                   | `string`                                                 | Primary email address                                   |\n| `alternateName`           | `string`                                                 | Alternative name your organization goes by              |\n| `foundingDate`            | `string`                                                 | ISO 8601 date when the organization was founded         |\n| `legalName`               | `string`                                                 | Registered legal name if different from name            |\n| `taxID`                   | `string`                                                 | Tax ID associated with your organization                |\n| `vatID`                   | `string`                                                 | VAT code (important trust signal)                       |\n| `duns`                    | `string`                                                 | Dun & Bradstreet DUNS number                            |\n| `leiCode`                 | `string`                                                 | Legal Entity Identifier (ISO 17442)                     |\n| `naics`                   | `string`                                                 | North American Industry Classification System code      |\n| `globalLocationNumber`    | `string`                                                 | GS1 Global Location Number                              |\n| `iso6523Code`             | `string`                                                 | ISO 6523 identifier (e.g., \"0199:724500PMK2A2M1SQQ228\") |\n| `numberOfEmployees`       | `number \\| QuantitativeValue`                            | Number of employees or range                            |\n| `review`                  | `Review \\| Review[]`                                     | A review or array of reviews of the organization        |\n| `aggregateRating`         | `AggregateRating`                                        | The overall rating based on a collection of reviews     |\n| `hasMerchantReturnPolicy` | `MerchantReturnPolicy \\| MerchantReturnPolicy[]`         | Return policy details (OnlineStore only)                |\n| `hasMemberProgram`        | `MemberProgram \\| MemberProgram[]`                       | Loyalty/membership program details (OnlineStore only)   |\n| `scriptId`                | `string`                                                 | Custom ID for the script tag                            |\n| `scriptKey`               | `string`                                                 | Custom key prop for React                               |\n\n#### Organization with Reviews and Ratings\n\n```tsx\n<OrganizationJsonLd\n  name=\"Acme Software Inc.\"\n  url=\"https://www.acmesoftware.com\"\n  logo=\"https://www.acmesoftware.com/logo.png\"\n  review={[\n    {\n      author: \"Sarah Johnson\",\n      reviewBody: \"Excellent company to work with!\",\n      reviewRating: {\n        ratingValue: 5,\n        bestRating: 5,\n      },\n      datePublished: \"2025-06-15\",\n    },\n    {\n      author: \"Michael Chen\",\n      reviewBody: \"Great software solutions with excellent customer service.\",\n      reviewRating: {\n        ratingValue: 4,\n        bestRating: 5,\n      },\n      datePublished: \"2025-08-22\",\n    },\n  ]}\n  aggregateRating={{\n    ratingValue: 4.6,\n    ratingCount: 312,\n    reviewCount: 245,\n    bestRating: 5,\n    worstRating: 1,\n  }}\n/>\n```\n\n#### OnlineStore with Loyalty Program Example\n\n```tsx\n<OrganizationJsonLd\n  type=\"OnlineStore\"\n  name=\"Example Store\"\n  url=\"https://www.example.com\"\n  hasMemberProgram={{\n    name: \"Rewards Plus\",\n    description:\n      \"Earn points and unlock exclusive benefits with our loyalty program\",\n    url: \"https://www.example.com/rewards\",\n    hasTiers: [\n      {\n        name: \"Bronze\",\n        hasTierBenefit: \"TierBenefitLoyaltyPoints\",\n        membershipPointsEarned: 1,\n      },\n      {\n        name: \"Silver\",\n        hasTierBenefit: [\"TierBenefitLoyaltyPoints\"],\n        hasTierRequirement: {\n          value: 500,\n          currency: \"USD\",\n        },\n        membershipPointsEarned: 2,\n      },\n      {\n        name: \"Gold\",\n        hasTierBenefit: [\"TierBenefitLoyaltyPoints\", \"TierBenefitLoyaltyPrice\"],\n        hasTierRequirement: {\n          name: \"Example Gold Credit Card\",\n        },\n        membershipPointsEarned: 5,\n        url: \"https://www.example.com/rewards/gold\",\n      },\n    ],\n  }}\n/>\n```\n\n#### Multiple Loyalty Programs Example\n\n```tsx\n<OrganizationJsonLd\n  type=\"OnlineStore\"\n  name=\"Premium Store\"\n  hasMemberProgram={[\n    {\n      name: \"Basic Rewards\",\n      description: \"Standard loyalty program for all customers\",\n      hasTiers: {\n        name: \"Member\",\n        hasTierBenefit: \"TierBenefitLoyaltyPoints\",\n        membershipPointsEarned: 1,\n      },\n    },\n    {\n      name: \"VIP Elite\",\n      description: \"Exclusive program for premium members\",\n      hasTiers: [\n        {\n          name: \"Silver VIP\",\n          hasTierBenefit: [\n            \"TierBenefitLoyaltyPoints\",\n            \"TierBenefitLoyaltyPrice\",\n          ],\n          hasTierRequirement: {\n            value: 2500,\n            currency: \"USD\",\n          },\n          membershipPointsEarned: {\n            value: 10,\n            unitText: \"points per dollar\",\n          },\n        },\n        {\n          name: \"Gold VIP\",\n          hasTierBenefit: [\n            \"TierBenefitLoyaltyPoints\",\n            \"TierBenefitLoyaltyPrice\",\n          ],\n          hasTierRequirement: {\n            price: 9.99,\n            priceCurrency: \"USD\",\n            billingDuration: 12,\n            billingIncrement: 1,\n            unitCode: \"MON\",\n          },\n          membershipPointsEarned: 20,\n        },\n      ],\n    },\n  ]}\n/>\n```\n\n#### MemberProgram Properties\n\n| Property      | Type                                       | Description                                   |\n| ------------- | ------------------------------------------ | --------------------------------------------- |\n| `name`        | `string`                                   | **Required**. Name of the loyalty program     |\n| `description` | `string`                                   | **Required**. Description of program benefits |\n| `url`         | `string`                                   | URL where customers can sign up               |\n| `hasTiers`    | `MemberProgramTier \\| MemberProgramTier[]` | **Required**. Tier(s) of the loyalty program  |\n\n#### MemberProgramTier Properties\n\n| Property                 | Type                          | Description                          |\n| ------------------------ | ----------------------------- | ------------------------------------ |\n| `name`                   | `string`                      | **Required**. Name of the tier       |\n| `hasTierBenefit`         | `string \\| string[]`          | **Required**. Benefits for this tier |\n| `hasTierRequirement`     | `various` (see below)         | Requirements to join this tier       |\n| `membershipPointsEarned` | `number \\| QuantitativeValue` | Points earned per unit spent         |\n| `url`                    | `string`                      | URL for tier-specific signup         |\n| `@id`                    | `string`                      | Unique identifier for the tier       |\n\n#### Tier Benefits\n\nBenefits can be specified using short names or full URLs:\n\n- `\"TierBenefitLoyaltyPoints\"` or `\"https://schema.org/TierBenefitLoyaltyPoints\"` - Earn loyalty points\n- `\"TierBenefitLoyaltyPrice\"` or `\"https://schema.org/TierBenefitLoyaltyPrice\"` - Special member pricing\n\n#### Tier Requirements\n\nThe `hasTierRequirement` property accepts different types based on the requirement:\n\n**Credit Card Requirement:**\n\n```tsx\nhasTierRequirement: {\n  name: \"Store Premium Credit Card\";\n}\n```\n\n**Minimum Spending Requirement (MonetaryAmount):**\n\n```tsx\nhasTierRequirement: {\n  value: 1000,\n  currency: \"USD\"\n}\n```\n\n**Subscription Fee (UnitPriceSpecification):**\n\n```tsx\nhasTierRequirement: {\n  price: 9.99,\n  priceCurrency: \"EUR\",\n  billingDuration: 12,      // Total duration\n  billingIncrement: 1,      // Billing frequency\n  unitCode: \"MON\"          // Unit (MON = monthly)\n}\n```\n\n**Text Description:**\n\n```tsx\nhasTierRequirement: \"By invitation only - must maintain $10,000+ annual spending\";\n```\n\n#### Membership Points Earned\n\nPoints can be specified as a simple number or as a detailed QuantitativeValue:\n\n**Simple:**\n\n```tsx\nmembershipPointsEarned: 5;\n```\n\n**Detailed:**\n\n```tsx\nmembershipPointsEarned: {\n  value: 10,\n  minValue: 10,\n  maxValue: 20,\n  unitText: \"points per dollar (double on special events)\"\n}\n```\n\n#### Best Practices\n\n1. **Place on homepage or about page**: Add this markup to your homepage or a dedicated \"about us\" page\n2. **Use specific subtypes**: Use \"OnlineStore\" for e-commerce sites rather than generic \"Organization\"\n3. **Include identifiers**: Add VAT ID, ISO codes, and other identifiers for better trust signals\n4. **Complete address information**: Provide full address details including country code\n5. **Multiple locations**: Use array format for addresses if you have multiple locations\n6. **High-quality logo**: Use a logo that looks good on white background, minimum 112x112px\n\n[↑ Back to Components](#-components-by-category)\n\n### LocalBusinessJsonLd\n\nThe `LocalBusinessJsonLd` component helps you add structured data for local businesses to improve their appearance in Google Search and Maps results, including knowledge panels and local business carousels.\n\n#### Basic Usage\n\n```tsx\nimport { LocalBusinessJsonLd } from \"next-seo\";\n\n<LocalBusinessJsonLd\n  type=\"Restaurant\"\n  name=\"Dave's Steak House\"\n  address={{\n    \"@type\": \"PostalAddress\",\n    streetAddress: \"148 W 51st St\",\n    addressLocality: \"New York\",\n    addressRegion: \"NY\",\n    postalCode: \"10019\",\n    addressCountry: \"US\",\n  }}\n  telephone=\"+12125551234\"\n  url=\"https://www.example.com\"\n  priceRange=\"$$$\"\n/>;\n```\n\n#### Restaurant Example with Full Details\n\n```tsx\n<LocalBusinessJsonLd\n  type=\"Restaurant\"\n  name=\"Dave's Steak House\"\n  address={{\n    \"@type\": \"PostalAddress\",\n    streetAddress: \"148 W 51st St\",\n    addressLocality: \"New York\",\n    addressRegion: \"NY\",\n    postalCode: \"10019\",\n    addressCountry: \"US\",\n  }}\n  geo={{\n    \"@type\": \"GeoCoordinates\",\n    latitude: 40.761293,\n    longitude: -73.982294,\n  }}\n  url=\"https://www.example.com/restaurant-locations/manhattan\"\n  telephone=\"+12122459600\"\n  image={[\n    \"https://example.com/photos/1x1/photo.jpg\",\n    \"https://example.com/photos/4x3/photo.jpg\",\n    \"https://example.com/photos/16x9/photo.jpg\",\n  ]}\n  servesCuisine=\"American\"\n  priceRange=\"$$$\"\n  openingHoursSpecification={[\n    {\n      \"@type\": \"OpeningHoursSpecification\",\n      dayOfWeek: [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"],\n      opens: \"11:30\",\n      closes: \"22:00\",\n    },\n    {\n      \"@type\": \"OpeningHoursSpecification\",\n      dayOfWeek: \"Saturday\",\n      opens: \"16:00\",\n      closes: \"23:00\",\n    },\n    {\n      \"@type\": \"OpeningHoursSpecification\",\n      dayOfWeek: \"Sunday\",\n      opens: \"16:00\",\n      closes: \"22:00\",\n    },\n  ]}\n  menu=\"https://www.example.com/menu\"\n  aggregateRating={{\n    \"@type\": \"AggregateRating\",\n    ratingValue: 4.5,\n    ratingCount: 250,\n  }}\n/>\n```\n\n#### Store with Departments\n\n```tsx\n<LocalBusinessJsonLd\n  type=\"Store\"\n  name=\"Dave's Department Store\"\n  address={{\n    \"@type\": \"PostalAddress\",\n    streetAddress: \"1600 Saratoga Ave\",\n    addressLocality: \"San Jose\",\n    addressRegion: \"CA\",\n    postalCode: \"95129\",\n    addressCountry: \"US\",\n  }}\n  telephone=\"+14088717984\"\n  department={[\n    {\n      type: \"Pharmacy\",\n      name: \"Dave's Pharmacy\",\n      address: \"1600 Saratoga Ave, San Jose, CA 95129\",\n      telephone: \"+14088719385\",\n      openingHoursSpecification: {\n        \"@type\": \"OpeningHoursSpecification\",\n        dayOfWeek: [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"],\n        opens: \"09:00\",\n        closes: \"19:00\",\n      },\n    },\n  ]}\n/>\n```\n\n#### Props\n\n| Property                    | Type                                                       | Description                                                                |\n| --------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------- |\n| `type`                      | `string \\| string[]`                                       | Business type (e.g., \"Restaurant\", \"Store\", or [\"Restaurant\", \"BarOrPub\"]) |\n| `name`                      | `string`                                                   | **Required.** The name of the business                                     |\n| `address`                   | `string \\| PostalAddress \\| (string \\| PostalAddress)[]`   | **Required.** Physical location(s) of the business                         |\n| `url`                       | `string`                                                   | The fully-qualified URL of the business location page                      |\n| `telephone`                 | `string`                                                   | Primary contact phone number (include country code)                        |\n| `image`                     | `string \\| ImageObject \\| (string \\| ImageObject)[]`       | Images of the business (multiple aspect ratios recommended)                |\n| `priceRange`                | `string`                                                   | Relative price range (e.g., \"$\", \"$$\", \"$$$\", or \"$10-15\")                 |\n| `geo`                       | `GeoCoordinates`                                           | Geographic coordinates (min 5 decimal places precision)                    |\n| `openingHoursSpecification` | `OpeningHoursSpecification \\| OpeningHoursSpecification[]` | Business hours including special/seasonal hours                            |\n| `review`                    | `Review \\| Review[]`                                       | Customer reviews (for review sites only)                                   |\n| `aggregateRating`           | `AggregateRating`                                          | Average rating based on multiple reviews                                   |\n| `department`                | `LocalBusinessBase \\| LocalBusinessBase[]`                 | Departments within the business                                            |\n| `menu`                      | `string`                                                   | URL of the menu (for food establishments)                                  |\n| `servesCuisine`             | `string \\| string[]`                                       | Type of cuisine served (for restaurants)                                   |\n| `sameAs`                    | `string \\| string[]`                                       | URLs of business profiles on other sites                                   |\n| `branchOf`                  | `Organization`                                             | Parent organization if this is a branch                                    |\n| `currenciesAccepted`        | `string`                                                   | Currencies accepted (e.g., \"USD\")                                          |\n| `paymentAccepted`           | `string`                                                   | Payment methods accepted                                                   |\n| `areaServed`                | `string \\| string[]`                                       | Geographic areas served                                                    |\n| `email`                     | `string`                                                   | Business email address                                                     |\n| `faxNumber`                 | `string`                                                   | Business fax number                                                        |\n| `slogan`                    | `string`                                                   | Business slogan or tagline                                                 |\n| `description`               | `string`                                                   | Detailed description of the business                                       |\n| `publicAccess`              | `boolean`                                                  | Whether the business location is accessible to the public                  |\n| `smokingAllowed`            | `boolean`                                                  | Whether smoking is allowed at the location                                 |\n| `isAccessibleForFree`       | `boolean`                                                  | Whether access is free                                                     |\n| `scriptId`                  | `string`                                                   | Custom ID for the script tag                                               |\n| `scriptKey`                 | `string`                                                   | Custom key prop for React                                                  |\n\n#### Opening Hours Examples\n\n**Standard Business Hours:**\n\n```tsx\nopeningHoursSpecification={[\n  {\n    \"@type\": \"OpeningHoursSpecification\",\n    dayOfWeek: [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"],\n    opens: \"09:00\",\n    closes: \"17:00\",\n  },\n  {\n    \"@type\": \"OpeningHoursSpecification\",\n    dayOfWeek: [\"Saturday\", \"Sunday\"],\n    opens: \"10:00\",\n    closes: \"16:00\",\n  },\n]}\n```\n\n**24/7 Operation:**\n\n```tsx\nopeningHoursSpecification={{\n  \"@type\": \"OpeningHoursSpecification\",\n  dayOfWeek: [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\", \"Sunday\"],\n  opens: \"00:00\",\n  closes: \"23:59\",\n}}\n```\n\n**Closed on Specific Days:**\n\n```tsx\nopeningHoursSpecification={{\n  \"@type\": \"OpeningHoursSpecification\",\n  dayOfWeek: \"Sunday\",\n  opens: \"00:00\",\n  closes: \"00:00\",\n}}\n```\n\n**Seasonal Hours:**\n\n```tsx\nopeningHoursSpecification={{\n  \"@type\": \"OpeningHoursSpecification\",\n  dayOfWeek: [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"],\n  opens: \"10:00\",\n  closes: \"18:00\",\n  validFrom: \"2024-06-01\",\n  validThrough: \"2024-09-30\",\n}}\n```\n\n#### Best Practices\n\n1. **Use specific business types**: Use the most specific LocalBusiness subtype (e.g., \"Restaurant\" instead of \"LocalBusiness\")\n2. **Multiple types**: For businesses that fit multiple categories, use an array (e.g., `[\"Restaurant\", \"BarOrPub\"]`)\n3. **Complete address**: Provide as many address fields as possible for better local SEO\n4. **High-quality images**: Include multiple images with different aspect ratios (16:9, 4:3, 1:1)\n5. **Accurate coordinates**: Use at least 5 decimal places for latitude and longitude\n6. **Opening hours**: Be precise with opening hours and include seasonal variations\n7. **Department naming**: Include the main store name with department name (e.g., \"Store Name - Pharmacy\")\n8. **Price range**: Keep under 100 characters; use standard symbols ($, $$, $$$) or ranges\n\n[↑ Back to Components](#-components-by-category)\n\n### MerchantReturnPolicyJsonLd\n\nThe `MerchantReturnPolicyJsonLd` component helps you add structured data for merchant return policies, enabling Google Search to display return policy information alongside your products and in knowledge panels. This component supports both detailed policy specifications and simple links to policy pages.\n\n#### Basic Usage - Option A (Detailed Properties)\n\nUse this pattern when you want to provide detailed return policy information:\n\n```tsx\nimport { MerchantReturnPolicyJsonLd } from \"next-seo\";\n\n<MerchantReturnPolicyJsonLd\n  applicableCountry={[\"US\", \"CA\"]}\n  returnPolicyCountry=\"US\"\n  returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n  merchantReturnDays={30}\n  returnMethod=\"https://schema.org/ReturnByMail\"\n  returnFees=\"https://schema.org/FreeReturn\"\n  refundType=\"https://schema.org/FullRefund\"\n  returnLabelSource=\"https://schema.org/ReturnLabelDownloadAndPrint\"\n/>;\n```\n\n#### Basic Usage - Option B (Link Only)\n\nUse this pattern when you prefer to link to your return policy page:\n\n```tsx\nimport { MerchantReturnPolicyJsonLd } from \"next-seo\";\n\n<MerchantReturnPolicyJsonLd merchantReturnLink=\"https://www.example.com/returns\" />;\n```\n\n#### Advanced Usage with All Features\n\n```tsx\nimport { MerchantReturnPolicyJsonLd } from \"next-seo\";\n\n<MerchantReturnPolicyJsonLd\n  applicableCountry={[\"DE\", \"AT\", \"CH\"]}\n  returnPolicyCountry=\"IE\"\n  returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n  merchantReturnDays={60}\n  itemCondition={[\n    \"https://schema.org/NewCondition\",\n    \"https://schema.org/DamagedCondition\",\n  ]}\n  returnMethod={[\n    \"https://schema.org/ReturnByMail\",\n    \"https://schema.org/ReturnInStore\",\n  ]}\n  returnFees=\"https://schema.org/ReturnShippingFees\"\n  returnShippingFeesAmount={{\n    value: 2.99,\n    currency: \"EUR\",\n  }}\n  refundType={[\n    \"https://schema.org/FullRefund\",\n    \"https://schema.org/ExchangeRefund\",\n  ]}\n  restockingFee={{\n    value: 10,\n    currency: \"EUR\",\n  }}\n  returnLabelSource=\"https://schema.org/ReturnLabelInBox\"\n  // Customer remorse specific\n  customerRemorseReturnFees=\"https://schema.org/ReturnShippingFees\"\n  customerRemorseReturnShippingFeesAmount={{\n    value: 5.99,\n    currency: \"EUR\",\n  }}\n  customerRemorseReturnLabelSource=\"https://schema.org/ReturnLabelDownloadAndPrint\"\n  // Item defect specific\n  itemDefectReturnFees=\"https://schema.org/FreeReturn\"\n  itemDefectReturnLabelSource=\"https://schema.org/ReturnLabelInBox\"\n  // Seasonal override\n  returnPolicySeasonalOverride={{\n    startDate: \"2025-12-01\",\n    endDate: \"2025-01-05\",\n    returnPolicyCategory: \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n    merchantReturnDays: 30,\n  }}\n/>;\n```\n\n#### Product-Level Return Policy\n\nYou can also specify return policies for individual products:\n\n```tsx\nimport { ProductJsonLd } from \"next-seo\";\n\n<ProductJsonLd\n  name=\"Premium Wireless Headphones\"\n  offers={{\n    price: 349.99,\n    priceCurrency: \"USD\",\n    hasMerchantReturnPolicy: {\n      applicableCountry: \"US\",\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 45,\n      returnFees: \"https://schema.org/FreeReturn\",\n      refundType: \"https://schema.org/FullRefund\",\n    },\n  }}\n/>;\n```\n\n#### Organization-Level Return Policy\n\nFor online stores, specify a standard return policy at the organization level:\n\n```tsx\nimport { OrganizationJsonLd } from \"next-seo\";\n\n<OrganizationJsonLd\n  type=\"OnlineStore\"\n  name=\"Example Store\"\n  hasMerchantReturnPolicy={{\n    applicableCountry: [\"US\", \"CA\"],\n    returnPolicyCategory: \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n    merchantReturnDays: 60,\n    returnFees: \"https://schema.org/FreeReturn\",\n    refundType: \"https://schema.org/FullRefund\",\n  }}\n/>;\n```\n\n#### Props\n\n| Property                                  | Type                                     | Description                                                |\n| ----------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- |\n| **Option A Properties**                   |\n| `applicableCountry`                       | `string \\| string[]`                     | **Required** (Option A). Countries where products are sold |\n| `returnPolicyCategory`                    | `string`                                 | **Required** (Option A). Type of return policy             |\n| `merchantReturnDays`                      | `number`                                 | Days for returns (required if finite window)               |\n| `returnPolicyCountry`                     | `string \\| string[]`                     | Countries where returns are processed                      |\n| `returnMethod`                            | `string \\| string[]`                     | How items can be returned                                  |\n| `returnFees`                              | `string`                                 | Type of return fees                                        |\n| `returnShippingFeesAmount`                | `MonetaryAmount`                         | Shipping fee for returns                                   |\n| `refundType`                              | `string \\| string[]`                     | Types of refunds available                                 |\n| `restockingFee`                           | `number \\| MonetaryAmount`               | Restocking fee (percentage or fixed)                       |\n| `returnLabelSource`                       | `string`                                 | How customers get return labels                            |\n| `itemCondition`                           | `string \\| string[]`                     | Acceptable return conditions                               |\n| **Customer Remorse Properties**           |\n| `customerRemorseReturnFees`               | `string`                                 | Fees for change-of-mind returns                            |\n| `customerRemorseReturnShippingFeesAmount` | `MonetaryAmount`                         | Shipping fee for remorse returns                           |\n| `customerRemorseReturnLabelSource`        | `string`                                 | Label source for remorse returns                           |\n| **Item Defect Properties**                |\n| `itemDefectReturnFees`                    | `string`                                 | Fees for defective item returns                            |\n| `itemDefectReturnShippingFeesAmount`      | `MonetaryAmount`                         | Shipping fee for defect returns                            |\n| `itemDefectReturnLabelSource`             | `string`                                 | Label source for defect returns                            |\n| **Seasonal Override**                     |\n| `returnPolicySeasonalOverride`            | `SeasonalOverride \\| SeasonalOverride[]` | Temporary policy changes                                   |\n| **Option B Property**                     |\n| `merchantReturnLink`                      | `string`                                 | URL to return policy page                                  |\n| **Component Properties**                  |\n| `scriptId`                                | `string`                                 | Custom ID for the script tag                               |\n| `scriptKey`                               | `string`                                 | Custom key for React rendering                             |\n\n#### Return Policy Categories\n\n- `https://schema.org/MerchantReturnFiniteReturnWindow` - Limited return period\n- `https://schema.org/MerchantReturnNotPermitted` - No returns allowed\n- `https://schema.org/MerchantReturnUnlimitedWindow` - Unlimited return period\n\n#### Return Methods\n\n- `https://schema.org/ReturnByMail` - Return by mail\n- `https://schema.org/ReturnInStore` - Return in store\n- `https://schema.org/ReturnAtKiosk` - Return at kiosk\n\n#### Return Fees\n\n- `https://schema.org/FreeReturn` - No charge for returns\n- `https://schema.org/ReturnFeesCustomerResponsibility` - Customer pays for return\n- `https://schema.org/ReturnShippingFees` - Specific shipping fee charged\n\n#### Refund Types\n\n- `https://schema.org/FullRefund` - Full monetary refund\n- `https://schema.org/ExchangeRefund` - Exchange for same product\n- `https://schema.org/StoreCreditRefund` - Store credit issued\n\n#### Best Practices\n\n1. **Choose the right option**: Use Option A for detailed policies, Option B for complex or frequently changing policies\n2. **Specify all countries**: List all countries where your policy applies\n3. **Different return scenarios**: Use customer remorse and item defect properties for different conditions\n4. **Seasonal variations**: Use seasonal overrides for holiday return windows\n5. **Product overrides**: Override organization-level policies for specific products when needed\n6. **Clear fee structure**: Be transparent about any fees customers will incur\n7. **Multiple return methods**: Offer multiple return options for customer convenience\n8. **Accurate time windows**: Ensure merchantReturnDays matches your actual policy\n\n[↑ Back to Components](#-components-by-category)\n\n### MovieCarouselJsonLd\n\nThe `MovieCarouselJsonLd` component helps you add structured data for movie carousels, enabling your movie lists to appear as rich results in Google Search on mobile devices. This component supports both summary page (URLs only) and all-in-one page (full movie data) patterns.\n\n#### Basic Usage - Summary Page Pattern\n\nUse this pattern when you have separate detail pages for each movie:\n\n```tsx\nimport { MovieCarouselJsonLd } from \"next-seo\";\n\n<MovieCarouselJsonLd\n  urls={[\n    \"https://example.com/movies/a-star-is-born\",\n    \"https://example.com/movies/bohemian-rhapsody\",\n    \"https://example.com/movies/black-panther\",\n  ]}\n/>;\n```\n\n#### All-in-One Page Pattern\n\nUse this pattern when all movie information is on a single page:\n\n```tsx\n<MovieCarouselJsonLd\n  movies={[\n    {\n      name: \"A Star Is Born\",\n      image: \"https://example.com/photos/6x9/star-is-born.jpg\",\n      dateCreated: \"2024-10-05\",\n      director: \"Bradley Cooper\",\n      review: {\n        reviewRating: { ratingValue: 5 },\n        author: \"John D.\",\n      },\n      aggregateRating: {\n        ratingValue: 90,\n        bestRating: 100,\n        ratingCount: 19141,\n      },\n    },\n    {\n      name: \"Bohemian Rhapsody\",\n      image: \"https://example.com/photos/6x9/bohemian.jpg\",\n      dateCreated: \"2024-11-02\",\n      director: \"Bryan Singer\",\n      aggregateRating: {\n        ratingValue: 61,\n        bestRating: 100,\n        ratingCount: 21985,\n      },\n    },\n  ]}\n/>\n```\n\n#### Advanced Example with All Features\n\n```tsx\n<MovieCarouselJsonLd\n  movies={[\n    {\n      name: \"Black Panther\",\n      url: \"https://example.com/movies/black-panther\",\n      image: [\n        \"https://example.com/photos/1x1/black-panther.jpg\",\n        \"https://example.com/photos/4x3/black-panther.jpg\",\n        \"https://example.com/photos/16x9/black-panther.jpg\",\n      ],\n      dateCreated: \"2024-02-16\",\n      director: {\n        name: \"Ryan Coogler\",\n        url: \"https://example.com/directors/ryan-coogler\",\n      },\n      review: {\n        reviewRating: {\n          ratingValue: 2,\n          bestRating: 5,\n        },\n        author: {\n          name: \"Trevor R.\",\n          url: \"https://example.com/reviewers/trevor\",\n        },\n        reviewBody:\n          \"While visually stunning, the plot fell short of expectations.\",\n        datePublished: \"2024-02-20\",\n      },\n      aggregateRating: {\n        ratingValue: 96,\n        bestRating: 100,\n        ratingCount: 88211,\n      },\n    },\n  ]}\n/>\n```\n\n#### Props\n\n| Property    | Type                                                | Description                                                      |\n| ----------- | --------------------------------------------------- | ---------------------------------------------------------------- |\n| `urls`      | `Array<string \\| {url: string; position?: number}>` | **Required for summary pattern.** URLs to individual movie pages |\n| `movies`    | `MovieListItem[]`                                   | **Required for all-in-one pattern.** Array of movie data         |\n| `scriptId`  | `string`                                            | Custom ID for the script tag                                     |\n| `scriptKey` | `string`                                            | Custom key prop for React                                        |\n\n#### MovieListItem Properties\n\n| Property          | Type                                                 | Description                                                     |\n| ----------------- | ---------------------------------------------------- | --------------------------------------------------------------- |\n| `name`            | `string`                                             | **Required.** The name of the movie                             |\n| `image`           | `string \\| ImageObject \\| (string \\| ImageObject)[]` | **Required.** Movie poster/image (6:9 aspect ratio recommended) |\n| `url`             | `string`                                             | URL to the movie's page                                         |\n| `dateCreated`     | `string`                                             | Release date in ISO 8601 format                                 |\n| `director`        | `string \\| Person`                                   | Movie director (accepts string or Person object)                |\n| `review`          | `Review`                                             | A review of the movie                                           |\n| `aggregateRating` | `AggregateRating`                                    | Average rating based on multiple reviews                        |\n\n#### Best Practices\n\n1. **Mobile-only feature**: Movie carousels only appear on mobile devices in Google Search\n2. **Image requirements**: Use 6:9 aspect ratio images (Google's requirement for movie carousels)\n3. **High-quality images**: Images must be high resolution and properly formatted (.jpg, .png, .gif)\n4. **Multiple images**: Consider providing multiple aspect ratios for better compatibility\n5. **Complete movie data**: Include as many properties as possible for richer search results\n6. **Consistent data**: All movies in the carousel must be from the same website\n7. **URL structure**: For summary pages, ensure all URLs point to pages on the same domain\n\n[↑ Back to Components](#-components-by-category)\n\n### BreadcrumbJsonLd\n\nThe `BreadcrumbJsonLd` component helps you add breadcrumb structured data to indicate a page's position in the site hierarchy. This can help Google display breadcrumb trails in search results, making it easier for users to understand and navigate your site structure.\n\n#### Basic Usage\n\n```tsx\nimport { BreadcrumbJsonLd } from \"next-seo\";\n\nexport default function ProductPage() {\n  return (\n    <>\n      <BreadcrumbJsonLd\n        items={[\n          {\n            name: \"Home\",\n            item: \"https://example.com\",\n          },\n          {\n            name: \"Products\",\n            item: \"https://example.com/products\",\n          },\n          {\n            name: \"Electronics\",\n            item: \"https://example.com/products/electronics\",\n          },\n          {\n            name: \"Headphones\",\n            item: \"https://example.com/products/electronics/headphones\",\n          },\n          {\n            name: \"Wireless Headphones XYZ\",\n          },\n        ]}\n      />\n      <main>\n        <h1>Wireless Headphones XYZ</h1>\n        {/* Product content */}\n      </main>\n    </>\n  );\n}\n```\n\n#### Multiple Breadcrumb Trails\n\nSome pages can be reached through multiple paths. You can specify multiple breadcrumb trails:\n\n```tsx\n<BreadcrumbJsonLd\n  multipleTrails={[\n    // First trail: Category path\n    [\n      {\n        name: \"Books\",\n        item: \"https://example.com/books\",\n      },\n      {\n        name: \"Science Fiction\",\n        item: \"https://example.com/books/sciencefiction\",\n      },\n      {\n        name: \"Award Winners\",\n      },\n    ],\n    // Second trail: Award path\n    [\n      {\n        name: \"Literature\",\n        item: \"https://example.com/literature\",\n      },\n      {\n        name: \"Award Winners\",\n      },\n    ],\n  ]}\n/>\n```\n\n#### Advanced Example with Thing Objects\n\nYou can use Thing objects with `@id` instead of plain URL strings:\n\n```tsx\n<BreadcrumbJsonLd\n  items={[\n    {\n      name: \"Home\",\n      item: \"https://example.com\",\n    },\n    {\n      name: \"Blog\",\n      item: { \"@id\": \"https://example.com/blog\" },\n    },\n    {\n      name: \"Technology\",\n      item: { \"@id\": \"https://example.com/blog/technology\" },\n    },\n    {\n      name: \"Understanding JSON-LD\",\n    },\n  ]}\n  scriptId=\"blog-breadcrumb\"\n  scriptKey=\"blog-breadcrumb-key\"\n/>\n```\n\n#### Props\n\n| Property         | Type                     | Description                                                      |\n| ---------------- | ------------------------ | ---------------------------------------------------------------- |\n| `items`          | `BreadcrumbListItem[]`   | Array of breadcrumb items (required if not using multipleTrails) |\n| `multipleTrails` | `BreadcrumbListItem[][]` | Array of breadcrumb trails (required if not using items)         |\n| `scriptId`       | `string`                 | Custom ID for the script tag                                     |\n| `scriptKey`      | `string`                 | Custom key prop for React                                        |\n\n**BreadcrumbListItem Type:**\n\n| Property | Type                          | Description                                            |\n| -------- | ----------------------------- | ------------------------------------------------------ |\n| `name`   | `string`                      | **Required.** The title of the breadcrumb              |\n| `item`   | `string \\| { \"@id\": string }` | URL or Thing object (optional for the last breadcrumb) |\n\n#### Best Practices\n\n1. **Omit the last item's URL**: The last breadcrumb (current page) typically shouldn't have an `item` property\n2. **Use logical hierarchy**: Breadcrumbs should represent a typical user path, not necessarily mirror URL structure\n3. **Keep names concise**: Use clear, descriptive names that help users understand the hierarchy\n4. **Multiple trails**: Use `multipleTrails` when a page can be logically reached through different paths\n5. **Include home**: Start trails from a logical entry point (often \"Home\") but it's not required\n6. **Avoid duplicates**: Each trail should represent a unique path to the page\n7. **Match visual breadcrumbs**: The structured data should match the breadcrumbs shown on your page\n\n[↑ Back to Components](#-components-by-category)\n\n### CarouselJsonLd\n\nThe `CarouselJsonLd` component helps you add structured data for carousels (ItemList) to enable rich results that display multiple cards from your site in a carousel format. This component supports Course, Movie, Recipe, and Restaurant content types.\n\n#### Basic Usage\n\n**Summary Page Pattern (URLs only):**\n\n```tsx\nimport { CarouselJsonLd } from \"next-seo\";\n\n// Simple array of URLs\n<CarouselJsonLd\n  urls={[\n    \"https://example.com/recipe/cookies\",\n    \"https://example.com/recipe/cake\",\n    \"https://example.com/recipe/pie\"\n  ]}\n/>\n\n// With custom positions\n<CarouselJsonLd\n  urls={[\n    { url: \"https://example.com/movie/matrix\", position: 1 },\n    \"https://example.com/movie/inception\", // position will be 2\n    { url: \"https://example.com/movie/interstellar\", position: 3 }\n  ]}\n/>\n```\n\n**All-in-One Page Pattern (Full Data):**\n\n```tsx\nimport { CarouselJsonLd } from \"next-seo\";\n\n// Course Carousel\n<CarouselJsonLd\n  contentType=\"Course\"\n  items={[\n    {\n      name: \"Introduction to React\",\n      description: \"Learn the fundamentals of React\",\n      url: \"https://example.com/courses/react\",\n      provider: \"Tech Academy\"\n    },\n    {\n      name: \"Advanced TypeScript\",\n      description: \"Master TypeScript features\",\n      provider: {\n        name: \"Code School\",\n        url: \"https://example.com/school\"\n      }\n    }\n  ]}\n/>\n\n// Movie Carousel\n<CarouselJsonLd\n  contentType=\"Movie\"\n  items={[\n    {\n      name: \"The Matrix\",\n      image: \"https://example.com/matrix.jpg\",\n      director: \"The Wachowskis\",\n      dateCreated: \"1999-03-31\",\n      aggregateRating: {\n        ratingValue: 8.7,\n        ratingCount: 1000\n      }\n    },\n    {\n      name: \"Inception\",\n      image: [\n        \"https://example.com/inception1.jpg\",\n        \"https://example.com/inception2.jpg\"\n      ],\n      director: { name: \"Christopher Nolan\" }\n    }\n  ]}\n/>\n\n// Recipe Carousel\n<CarouselJsonLd\n  contentType=\"Recipe\"\n  items={[\n    {\n      name: \"Chocolate Chip Cookies\",\n      image: \"https://example.com/cookies.jpg\",\n      description: \"Classic chocolate chip cookies\",\n      author: \"Chef John\",\n      prepTime: \"PT20M\",\n      cookTime: \"PT12M\",\n      recipeYield: 24,\n      recipeIngredient: [\n        \"2 cups flour\",\n        \"1 cup butter\",\n        \"1 cup chocolate chips\"\n      ],\n      aggregateRating: {\n        ratingValue: 4.8,\n        ratingCount: 250\n      }\n    }\n  ]}\n/>\n\n// Restaurant Carousel\n<CarouselJsonLd\n  contentType=\"Restaurant\"\n  items={[\n    {\n      name: \"Joe's Pizza\",\n      address: \"123 Main St, New York, NY 10001\",\n      telephone: \"+1-212-555-0100\",\n      servesCuisine: [\"Italian\", \"Pizza\"],\n      priceRange: \"$$\",\n      aggregateRating: {\n        ratingValue: 4.5,\n        ratingCount: 500\n      },\n      geo: {\n        latitude: 40.7128,\n        longitude: -74.0060\n      }\n    }\n  ]}\n/>\n```\n\n#### Advanced Examples\n\n**Recipe Carousel with Full Details:**\n\n```tsx\n<CarouselJsonLd\n  contentType=\"Recipe\"\n  items={[\n    {\n      name: \"Perfect Pancakes\",\n      image: [\n        \"https://example.com/pancakes1.jpg\",\n        \"https://example.com/pancakes2.jpg\",\n      ],\n      description: \"Fluffy and delicious pancakes\",\n      author: [\n        \"Chef Alice\",\n        { name: \"Chef Bob\", url: \"https://example.com/chefs/bob\" },\n      ],\n      datePublished: \"2024-01-01\",\n      prepTime: \"PT10M\",\n      cookTime: \"PT15M\",\n      totalTime: \"PT25M\",\n      recipeYield: \"4 servings\",\n      recipeCategory: \"Breakfast\",\n      recipeCuisine: \"American\",\n      recipeIngredient: [\n        \"2 cups all-purpose flour\",\n        \"2 tablespoons sugar\",\n        \"2 eggs\",\n        \"1 1/2 cups milk\",\n      ],\n      recipeInstructions: [\n        \"Mix dry ingredients in a bowl\",\n        { text: \"Whisk wet ingredients separately\" },\n        {\n          name: \"Cooking\",\n          itemListElement: [\n            { text: \"Heat griddle to 375°F\" },\n            { text: \"Pour batter and cook until bubbles form\" },\n            { text: \"Flip and cook until golden\" },\n          ],\n        },\n      ],\n      nutrition: {\n        calories: \"250 calories\",\n        proteinContent: \"8g\",\n        carbohydrateContent: \"35g\",\n        fatContent: \"9g\",\n      },\n      aggregateRating: {\n        ratingValue: 4.9,\n        ratingCount: 1200,\n      },\n      video: {\n        name: \"How to Make Perfect Pancakes\",\n        description: \"Step-by-step video guide\",\n        thumbnailUrl: \"https://example.com/pancakes-thumb.jpg\",\n        contentUrl: \"https://example.com/pancakes-video.mp4\",\n        uploadDate: \"2024-01-01\",\n        duration: \"PT5M30S\",\n      },\n      keywords: \"pancakes, breakfast, easy recipe\",\n    },\n  ]}\n/>\n```\n\n**Restaurant Carousel with Opening Hours:**\n\n```tsx\n<CarouselJsonLd\n  contentType=\"Restaurant\"\n  items={[\n    {\n      name: \"Fine Dining Restaurant\",\n      address: {\n        streetAddress: \"456 Oak Avenue\",\n        addressLocality: \"San Francisco\",\n        addressRegion: \"CA\",\n        postalCode: \"94102\",\n        addressCountry: \"US\",\n      },\n      image: [\n        \"https://example.com/restaurant1.jpg\",\n        \"https://example.com/restaurant2.jpg\",\n      ],\n      telephone: \"+1-415-555-0200\",\n      url: \"https://example.com/restaurant\",\n      menu: \"https://example.com/restaurant/menu\",\n      servesCuisine: [\"French\", \"Mediterranean\"],\n      priceRange: \"$$$\",\n      openingHoursSpecification: [\n        {\n          dayOfWeek: [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\"],\n          opens: \"17:00\",\n          closes: \"22:00\",\n        },\n        {\n          dayOfWeek: [\"Friday\", \"Saturday\"],\n          opens: \"17:00\",\n          closes: \"23:00\",\n        },\n      ],\n      review: [\n        {\n          reviewRating: { ratingValue: 5 },\n          author: \"Food Critic\",\n          reviewBody: \"Exceptional dining experience\",\n        },\n      ],\n      aggregateRating: {\n        ratingValue: 4.7,\n        bestRating: 5,\n        ratingCount: 850,\n      },\n    },\n  ]}\n/>\n```\n\n#### Props\n\n| Property      | Type                                                              | Description                                      |\n| ------------- | ----------------------------------------------------------------- | ------------------------------------------------ |\n| `urls`        | `SummaryPageItem[]`                                               | Array of URLs for summary page pattern           |\n| `contentType` | `\"Course\" \\| \"Movie\" \\| \"Recipe\" \\| \"Restaurant\"`                 | Type of content in the carousel (for all-in-one) |\n| `items`       | `CourseItem[] \\| MovieItem[] \\| RecipeItem[] \\| RestaurantItem[]` | Array of items matching the content type         |\n| `scriptId`    | `string`                                                          | Custom ID for the script tag                     |\n| `scriptKey`   | `string`                                                          | Custom key prop for React                        |\n\n**SummaryPageItem Type:**\n\n| Type                                 | Description                       |\n| ------------------------------------ | --------------------------------- |\n| `string`                             | Simple URL string                 |\n| `{ url: string; position?: number }` | URL with optional custom position |\n\n#### Best Practices\n\n1. **Choose the right pattern**:\n   - Use **summary page pattern** when you have separate detail pages for each item\n   - Use **all-in-one pattern** when all content is on a single page\n\n2. **Consistent content types**: All items in a carousel must be of the same type (e.g., all recipes or all movies)\n\n3. **Required images**:\n   - Movies require at least one image\n   - Recipes should include images for better visibility\n   - Use multiple aspect ratios when possible\n\n4. **Position numbering**:\n   - Positions start at 1, not 0\n   - If not specified, positions are auto-assigned sequentially\n\n5. **URL structure**: For summary pages, ensure all URLs point to pages on the same domain\n\n6. **Rich content**: Include as much relevant information as possible for better search results\n\n7. **Validation**: Test your structured data with Google's Rich Results Test\n\n[↑ Back to Components](#-components-by-category)\n\n### CourseJsonLd\n\nThe `CourseJsonLd` component helps you add structured data for courses to enable course list rich results in Google Search. This can help prospective students discover your courses more easily.\n\n#### Basic Usage\n\n**Single Course:**\n\n```tsx\nimport { CourseJsonLd } from \"next-seo\";\n\n<CourseJsonLd\n  name=\"Introduction to Computer Science\"\n  description=\"An introductory CS course laying out the basics.\"\n  provider=\"University of Technology\"\n/>;\n```\n\n**Course List:**\n\n```tsx\nimport { CourseJsonLd } from \"next-seo\";\n\n// Summary page pattern - just URLs\n<CourseJsonLd\n  type=\"list\"\n  urls={[\n    \"https://example.com/courses/intro-cs\",\n    \"https://example.com/courses/intermediate-cs\",\n    \"https://example.com/courses/advanced-cs\"\n  ]}\n/>\n\n// All-in-one page pattern - full course data\n<CourseJsonLd\n  type=\"list\"\n  courses={[\n    {\n      name: \"Introduction to Programming\",\n      description: \"Learn the basics of programming.\",\n      url: \"https://example.com/courses/intro-programming\",\n      provider: \"Tech Institute\"\n    },\n    {\n      name: \"Advanced Algorithms\",\n      description: \"Study complex algorithmic solutions.\",\n      provider: {\n        name: \"University Online\",\n        sameAs: \"https://university.edu\"\n      }\n    }\n  ]}\n/>\n```\n\n#### Props\n\n**Single Course Props:**\n\n| Property      | Type                                                    | Description                                                           |\n| ------------- | ------------------------------------------------------- | --------------------------------------------------------------------- |\n| `type`        | `\"single\"`                                              | Optional. Explicitly sets single course pattern                       |\n| `name`        | `string`                                                | **Required.** The title of the course                                 |\n| `description` | `string`                                                | **Required.** A description of the course (60 char limit for display) |\n| `url`         | `string`                                                | The URL of the course page                                            |\n| `provider`    | `string \\| Organization \\| Omit<Organization, \"@type\">` | The organization offering the course                                  |\n| `scriptId`    | `string`                                                | Custom ID for the script tag                                          |\n| `scriptKey`   | `string`                                                | Custom key for React reconciliation                                   |\n\n**Course List Props:**\n\n| Property    | Type                                               | Description                                |\n| ----------- | -------------------------------------------------- | ------------------------------------------ |\n| `type`      | `\"list\"`                                           | **Required.** Sets the course list pattern |\n| `urls`      | `(string \\| { url: string; position?: number })[]` | URLs for summary page pattern              |\n| `courses`   | `CourseListItem[]`                                 | Full course data for all-in-one pattern    |\n| `scriptId`  | `string`                                           | Custom ID for the script tag               |\n| `scriptKey` | `string`                                           | Custom key for React reconciliation        |\n\n#### Advanced Example\n\n```tsx\nimport { CourseJsonLd } from \"next-seo\";\n\nexport default function CourseCatalogPage() {\n  return (\n    <>\n      <CourseJsonLd\n        type=\"list\"\n        courses={[\n          {\n            name: \"Full-Stack Web Development\",\n            description: \"Master modern web development from front to back.\",\n            url: \"https://example.com/courses/fullstack\",\n            provider: {\n              name: \"Code Academy\",\n              url: \"https://codeacademy.com\",\n              sameAs: [\n                \"https://twitter.com/codeacademy\",\n                \"https://linkedin.com/company/codeacademy\",\n              ],\n            },\n          },\n          {\n            name: \"Data Science with Python\",\n            description:\n              \"Learn data analysis and machine learning with Python.\",\n            url: \"https://example.com/courses/data-science\",\n            provider: \"Tech University\",\n          },\n          {\n            name: \"Mobile App Development\",\n            description: \"Build iOS and Android apps with React Native.\",\n            url: \"https://example.com/courses/mobile-dev\",\n            provider: {\n              name: \"Mobile Dev Institute\",\n              logo: \"https://example.com/logo.png\",\n            },\n          },\n        ]}\n      />\n\n      <h1>Our Course Catalog</h1>\n      {/* Your course list UI */}\n    </>\n  );\n}\n```\n\n#### Best Practices\n\n1. **Minimum of 3 courses**: Google requires at least 3 courses for course list rich results\n2. **Consistent provider**: Use the same format for provider across all courses\n3. **Description length**: Keep descriptions under 60 characters for optimal display\n4. **Valid URLs**: Ensure all course URLs are accessible and on the same domain\n5. **Choose the right pattern**:\n   - Use **summary page** pattern when courses have their own detail pages\n   - Use **all-in-one** pattern when all course information is on a single page\n6. **Avoid promotional content**: Don't include prices, discounts, or marketing language in course names\n\n### EventJsonLd\n\nThe `EventJsonLd` component helps you add structured data for events to improve their discoverability in Google Search results and other Google products like Google Maps. Events can appear with rich features including images, dates, locations, and ticket information.\n\n#### Basic Usage\n\n```tsx\nimport { EventJsonLd } from \"next-seo\";\n\n<EventJsonLd\n  name=\"The Adventures of Kira and Morrison\"\n  startDate=\"2025-07-21T19:00-05:00\"\n  location=\"Snickerpark Stadium\"\n/>;\n```\n\n#### Standard Event Example\n\n```tsx\n<EventJsonLd\n  name=\"The Adventures of Kira and Morrison\"\n  startDate=\"2025-07-21T19:00-05:00\"\n  endDate=\"2025-07-21T23:00-05:00\"\n  location={{\n    \"@type\": \"Place\",\n    name: \"Snickerpark Stadium\",\n    address: {\n      \"@type\": \"PostalAddress\",\n      streetAddress: \"100 West Snickerpark Dr\",\n      addressLocality: \"Snickertown\",\n      postalCode: \"19019\",\n      addressRegion: \"PA\",\n      addressCountry: \"US\",\n    },\n  }}\n  description=\"The Adventures of Kira and Morrison is coming to Snickertown in a can't miss performance.\"\n  image={[\n    \"https://example.com/photos/1x1/photo.jpg\",\n    \"https://example.com/photos/4x3/photo.jpg\",\n    \"https://example.com/photos/16x9/photo.jpg\",\n  ]}\n  offers={{\n    \"@type\": \"Offer\",\n    url: \"https://www.example.com/event_offer/12345_202403180430\",\n    price: 30,\n    priceCurrency: \"USD\",\n    availability: \"https://schema.org/InStock\",\n    validFrom: \"2024-05-21T12:00\",\n  }}\n  performer={{\n    \"@type\": \"PerformingGroup\",\n    name: \"Kira and Morrison\",\n  }}\n  organizer={{\n    \"@type\": \"Organization\",\n    name: \"Kira and Morrison Music\",\n    url: \"https://kiraandmorrisonmusic.com\",\n  }}\n/>\n```\n\n#### Event Status Examples\n\n##### Cancelled Event\n\n```tsx\n<EventJsonLd\n  name=\"Summer Festival 2025\"\n  startDate=\"2025-08-15T12:00:00\"\n  location=\"City Park\"\n  eventStatus=\"https://schema.org/EventCancelled\"\n/>\n```\n\n##### Rescheduled Event\n\n```tsx\n<EventJsonLd\n  name=\"Tech Conference 2025\"\n  startDate=\"2025-09-20T09:00:00\"\n  location=\"Convention Center\"\n  eventStatus=\"https://schema.org/EventRescheduled\"\n  previousStartDate=\"2025-07-15T09:00:00\"\n/>\n```\n\n#### Props\n\n| Property            | Type                                                 | Description                                                                        |\n| ------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| `name`              | `string`                                             | **Required.** The full title of the event                                          |\n| `startDate`         | `string`                                             | **Required.** Start date/time in ISO-8601 format                                   |\n| `location`          | `string \\| Place`                                    | **Required.** Event venue (string or Place object)                                 |\n| `endDate`           | `string`                                             | End date/time in ISO-8601 format                                                   |\n| `description`       | `string`                                             | Detailed description of the event                                                  |\n| `eventStatus`       | `EventStatusType`                                    | Status: EventScheduled (default), EventCancelled, EventPostponed, EventRescheduled |\n| `image`             | `string \\| ImageObject \\| (string \\| ImageObject)[]` | Event images (recommended: multiple aspect ratios)                                 |\n| `offers`            | `Offer \\| Offer[]`                                   | Ticket/pricing information                                                         |\n| `performer`         | `string \\| Person \\| PerformingGroup \\| array`       | Performers at the event                                                            |\n| `organizer`         | `string \\| Person \\| Organization`                   | Event host/organizer                                                               |\n| `previousStartDate` | `string \\| string[]`                                 | Previous date(s) for rescheduled events                                            |\n| `url`               | `string`                                             | URL of the event page                                                              |\n| `scriptId`          | `string`                                             | Custom ID for the script tag                                                       |\n| `scriptKey`         | `string`                                             | Custom key prop for React                                                          |\n\n#### Offer Type\n\n| Property        | Type     | Description                                      |\n| --------------- | -------- | ------------------------------------------------ |\n| `url`           | `string` | URL to purchase tickets                          |\n| `price`         | `number` | Lowest available price (use 0 for free events)   |\n| `priceCurrency` | `string` | 3-letter ISO 4217 currency code (e.g., \"USD\")    |\n| `availability`  | `string` | Availability status (InStock, SoldOut, PreOrder) |\n| `validFrom`     | `string` | Date/time when tickets go on sale                |\n\n#### Best Practices\n\n1. **Date/Time Format**: Always use ISO-8601 format with timezone offset (e.g., `2025-07-21T19:00-05:00`)\n2. **Day-long Events**: For all-day events, use date only format (e.g., `2025-07-04`)\n3. **Location Details**: Provide complete address information for better discoverability\n4. **Multiple Images**: Include images in different aspect ratios (16:9, 4:3, 1:1) for various display contexts\n5. **Event Status**: Keep original dates when cancelling/postponing; only update the `eventStatus`\n6. **Free Events**: Set `price: 0` for events without charge\n7. **Multiple Performers**: Use an array when listing multiple artists or speakers\n8. **Rescheduled Events**: Always include `previousStartDate` when using `EventRescheduled` status\n\n#### Date and Time Guidelines\n\n- **Include timezone**: Specify UTC/GMT offset (e.g., `-05:00` for EST)\n- **Multi-day events**: Set both `startDate` and `endDate`\n- **Unknown end time**: Omit `endDate` rather than guessing\n- **Date-only format**: Use for all-day events (e.g., festivals)\n\nExample timezone handling:\n\n```tsx\n// New York event during standard time\nstartDate: \"2025-12-21T19:00:00-05:00\";\n\n// California event during daylight saving time\nstartDate: \"2025-07-21T19:00:00-07:00\";\n\n// All-day event\nstartDate: \"2025-07-04\";\nendDate: \"2025-07-04\";\n```\n\n### FAQJsonLd\n\nThe `FAQJsonLd` component helps you add structured data for frequently asked questions (FAQ) pages. This can help your FAQ content appear as rich results in Google Search, making it easier for users to find answers to common questions.\n\n> **Note**: FAQ rich results are only available for well-known, authoritative government or health websites. However, implementing proper FAQ structured data is still valuable for SEO and can help search engines better understand your content.\n\n#### Basic Usage\n\n```tsx\nimport { FAQJsonLd } from \"next-seo\";\n\nexport default function FAQPage() {\n  return (\n    <>\n      <FAQJsonLd\n        questions={[\n          {\n            question: \"How to find an apprenticeship?\",\n            answer:\n              \"We provide an official service to search through available apprenticeships. To get started, create an account here, specify the desired region, and your preferences.\",\n          },\n          {\n            question: \"Whom to contact?\",\n            answer:\n              \"You can contact the apprenticeship office through our official phone hotline above, or with the web-form below.\",\n          },\n        ]}\n      />\n      <h1>Frequently Asked Questions</h1>\n      {/* Your FAQ content */}\n    </>\n  );\n}\n```\n\n#### Advanced Example with HTML Content\n\nFAQ answers support HTML content including links, lists, and formatting:\n\n```tsx\n<FAQJsonLd\n  questions={[\n    {\n      question: \"What documents are required for application?\",\n      answer: `\n        <p>You'll need to provide the following documents:</p>\n        <ul>\n          <li>Valid government-issued ID</li>\n          <li>High school diploma or equivalent</li>\n          <li>Proof of residence</li>\n          <li><a href=\"/forms/medical\">Medical clearance form</a></li>\n        </ul>\n        <p>All documents must be submitted within 30 days of application.</p>\n      `,\n    },\n    {\n      question: \"How long does the application process take?\",\n      answer:\n        \"<p>The typical processing time is <strong>7-10 business days</strong> from the date we receive all required documents.</p>\",\n    },\n  ]}\n  scriptId=\"faq-structured-data\"\n/>\n```\n\n#### Different Input Formats\n\nThe component supports multiple input formats for flexibility:\n\n```tsx\n// Simple question/answer format (recommended)\n<FAQJsonLd\n  questions={[\n    {\n      question: \"What is the cost?\",\n      answer: \"The program is free for eligible participants.\",\n    },\n  ]}\n/>\n\n// Schema.org name/acceptedAnswer format\n<FAQJsonLd\n  questions={[\n    {\n      name: \"What is the cost?\",\n      acceptedAnswer: \"The program is free for eligible participants.\",\n    },\n  ]}\n/>\n\n// With Answer object\n<FAQJsonLd\n  questions={[\n    {\n      name: \"What is the cost?\",\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: \"The program is free for eligible participants.\",\n      },\n    },\n  ]}\n/>\n```\n\n#### Props\n\n| Property    | Type              | Description                                                                             |\n| ----------- | ----------------- | --------------------------------------------------------------------------------------- |\n| `questions` | `QuestionInput[]` | **Required.** Array of questions and answers. See input formats below.                  |\n| `scriptId`  | `string`          | Optional. Sets the `id` attribute on the script tag.                                    |\n| `scriptKey` | `string`          | Optional. Sets the `data-testid` attribute on the script tag. Defaults to \"faq-jsonld\". |\n\n#### Question Input Formats\n\nThe `questions` array accepts several formats:\n\n1. **Simple object** (recommended):\n\n   ```tsx\n   { question: \"string\", answer: \"string\" }\n   ```\n\n2. **Schema.org format**:\n\n   ```tsx\n   { name: \"string\", acceptedAnswer: \"string\" }\n   ```\n\n3. **Full Answer object**:\n   ```tsx\n   {\n     name: \"string\",\n     acceptedAnswer: {\n       \"@type\": \"Answer\",\n       text: \"string\"\n     }\n   }\n   ```\n\n#### Best Practices\n\n1. **Include complete Q&A**: Each question and answer should contain the full text that appears on your page\n2. **Use HTML wisely**: Google supports these HTML tags in answers: `<h1>` through `<h6>`, `<br>`, `<ol>`, `<ul>`, `<li>`, `<a>`, `<p>`, `<div>`, `<b>`, `<strong>`, `<i>`, and `<em>`\n3. **Match page content**: The FAQ structured data must match the visible Q&A content on your page\n4. **Avoid promotional content**: Don't use FAQPage for advertising purposes\n5. **One instance per page**: If the same FAQ appears on multiple pages, only mark it up on one page\n6. **Expandable sections**: It's fine if answers are hidden behind expandable sections, as long as users can access them\n7. **No user submissions**: FAQPage is for questions with single, authoritative answers. For user-generated Q&A, use QAPage instead\n\n### ImageJsonLd\n\nThe `ImageJsonLd` component helps you add structured data for images to improve their appearance in Google Images. This enables features like the Licensable badge and displays metadata such as creator, credit, copyright, and licensing information.\n\n#### Basic Usage\n\n```tsx\nimport { ImageJsonLd } from \"next-seo\";\n\n<ImageJsonLd\n  contentUrl=\"https://example.com/photos/black-labrador-puppy.jpg\"\n  creator=\"Brixton Brownstone\"\n  license=\"https://example.com/license\"\n  acquireLicensePage=\"https://example.com/how-to-use-my-images\"\n  creditText=\"Labrador PhotoLab\"\n  copyrightNotice=\"Clara Kent\"\n/>;\n```\n\n#### Advanced Usage - Organization Creator\n\n```tsx\n<ImageJsonLd\n  contentUrl=\"https://example.com/photos/product-photo.jpg\"\n  creator={{\n    name: \"PhotoLab Studios\",\n    logo: \"https://example.com/photolab-logo.jpg\",\n    sameAs: [\"https://twitter.com/photolab\", \"https://instagram.com/photolab\"],\n  }}\n  license=\"https://creativecommons.org/licenses/by-nc/4.0/\"\n  acquireLicensePage=\"https://example.com/licensing\"\n  creditText=\"PhotoLab Studios\"\n  copyrightNotice=\"© 2024 PhotoLab Studios\"\n/>\n```\n\n#### Multiple Images\n\n```tsx\n<ImageJsonLd\n  images={[\n    {\n      contentUrl: \"https://example.com/photos/black-labrador-puppy.jpg\",\n      creator: \"Brixton Brownstone\",\n      license: \"https://example.com/license\",\n      creditText: \"Labrador PhotoLab\",\n    },\n    {\n      contentUrl: \"https://example.com/photos/adult-black-labrador.jpg\",\n      creator: [\n        \"Brixton Brownstone\",\n        {\n          name: \"Clara Kent\",\n          url: \"https://clarakent.com\",\n        },\n      ],\n      copyrightNotice: \"© 2024 Clara Kent\",\n      license: \"https://example.com/license\",\n    },\n  ]}\n/>\n```\n\n#### Props\n\n| Property             | Type                 | Description                                                                                                          |\n| -------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------- |\n| `contentUrl`         | `string`             | **Required.** The URL of the actual image content                                                                    |\n| `creator`            | `Author \\| Author[]` | The creator(s) of the image (photographer, designer, etc.). Can be string name(s), Person, or Organization object(s) |\n| `creditText`         | `string`             | The name of the person/organization credited when the image is published                                             |\n| `copyrightNotice`    | `string`             | The copyright notice for claiming intellectual property                                                              |\n| `license`            | `string`             | URL to a page describing the license governing the image's use                                                       |\n| `acquireLicensePage` | `string`             | URL to a page where users can find information on how to license the image                                           |\n| `images`             | `Array<ImageObject>` | Array of image objects with the above properties (for multiple images)                                               |\n| `scriptId`           | `string`             | Custom ID for the script tag                                                                                         |\n| `scriptKey`          | `string`             | Custom key for script deduplication                                                                                  |\n\n> **Note**: You must include `contentUrl` and at least one of: `creator`, `creditText`, `copyrightNotice`, or `license` for the image to be eligible for enhancements like the Licensable badge.\n\n#### Best Practices\n\n1. **Always provide licensing information**: Include the `license` property to make your images eligible for the Licensable badge\n2. **Credit creators properly**: Use structured creator information to ensure proper attribution\n3. **Include acquire license page**: Help users understand how they can legally use your images\n4. **Use consistent copyright notices**: Maintain clear copyright information across your images\n5. **Multiple creators**: When multiple people contributed to an image, list all creators\n6. **Organization vs Person**: Use Organization type for companies/studios, Person type for individuals\n\n### QuizJsonLd\n\nThe `QuizJsonLd` component helps you add structured data for educational quizzes and flashcards. This can help your educational content appear in Google's education Q&A carousel when users search for educational topics.\n\n#### Basic Usage\n\n```tsx\nimport { QuizJsonLd } from \"next-seo\";\n\nexport default function BiologyQuizPage() {\n  return (\n    <>\n      <QuizJsonLd\n        questions={[\n          {\n            question: \"What is the powerhouse of the cell?\",\n            answer: \"Mitochondria\",\n          },\n          {\n            question:\n              \"What process do plants use to convert sunlight into energy?\",\n            answer: \"Photosynthesis\",\n          },\n        ]}\n        about=\"Cell Biology\"\n        educationalAlignment={[\n          {\n            type: \"educationalSubject\",\n            name: \"Biology\",\n          },\n          {\n            type: \"educationalLevel\",\n            name: \"Grade 10\",\n          },\n        ]}\n      />\n      {/* Your quiz content */}\n    </>\n  );\n}\n```\n\n#### Props\n\n| Property               | Type                                                                      | Description                                                  |\n| ---------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------ |\n| `questions`            | `QuestionInput[]`                                                         | **Required.** Array of flashcard questions and answers       |\n| `about`                | `string \\| Thing`                                                         | The subject or topic of the quiz                             |\n| `educationalAlignment` | `Array<{type: \"educationalSubject\" \\| \"educationalLevel\", name: string}>` | Educational alignments specifying subject and/or grade level |\n| `scriptId`             | `string`                                                                  | Custom ID for the script tag                                 |\n| `scriptKey`            | `string`                                                                  | Custom key for React (defaults to \"quiz-jsonld\")             |\n\n#### Question Formats\n\nThe `questions` array accepts several formats:\n\n1. **Simple object** (recommended):\n\n   ```tsx\n   { question: \"What is 2 + 2?\", answer: \"4\" }\n   ```\n\n2. **String format** (for fact-based flashcards):\n\n   ```tsx\n   \"The Earth revolves around the Sun in 365.25 days\";\n   ```\n\n3. **Text/acceptedAnswer format**:\n\n   ```tsx\n   { text: \"What is DNA?\", acceptedAnswer: \"Deoxyribonucleic acid\" }\n   ```\n\n4. **Full Answer object**:\n   ```tsx\n   {\n     text: \"Explain photosynthesis\",\n     acceptedAnswer: {\n       \"@type\": \"Answer\",\n       text: \"The process by which plants convert light energy into chemical energy\"\n     }\n   }\n   ```\n\n#### Advanced Example\n\n```tsx\n<QuizJsonLd\n  questions={[\n    // Simple flashcard fact\n    \"The mitochondria is the powerhouse of the cell\",\n    // Question/answer format\n    {\n      question: \"What are the four bases of DNA?\",\n      answer: \"Adenine (A), Thymine (T), Guanine (G), and Cytosine (C)\",\n    },\n    // Full format with Answer object\n    {\n      text: \"Describe the water cycle\",\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: \"The continuous movement of water through evaporation, condensation, precipitation, and collection\",\n      },\n    },\n  ]}\n  about={{\n    name: \"Biology Fundamentals\",\n    description: \"Core concepts in cellular and molecular biology\",\n    url: \"https://example.com/biology-course\",\n  }}\n  educationalAlignment={[\n    {\n      type: \"educationalSubject\",\n      name: \"Biology\",\n    },\n    {\n      type: \"educationalLevel\",\n      name: \"High School\",\n    },\n  ]}\n/>\n```\n\n#### Best Practices\n\n1. **Educational content only**: Quiz structured data is specifically for educational flashcards and Q&A\n2. **Use \"Flashcard\" type**: All questions automatically use `eduQuestionType: \"Flashcard\"` as required by Google\n3. **Clear answers**: Provide concise, factual answers appropriate for the educational level\n4. **Subject alignment**: Always specify the educational subject using `educationalAlignment`\n5. **Grade level**: Include the target grade or educational level when applicable\n6. **Match visible content**: The structured data should match the quiz content displayed on your page\n7. **Single answer format**: Each question should have one clear, authoritative answer\n\n> **Note**: The education Q&A carousel is available when searching for education-related topics in English, Portuguese, Spanish (Mexico), and Vietnamese.\n\n### DatasetJsonLd\n\nThe `DatasetJsonLd` component helps you add structured data for datasets, making them easier to find in Google's Dataset Search. This is ideal for scientific data, government data, machine learning datasets, and any other structured data collections.\n\n#### Basic Usage\n\n```tsx\nimport { DatasetJsonLd } from \"next-seo\";\n\nexport default function DatasetPage() {\n  return (\n    <>\n      <DatasetJsonLd\n        name=\"NCDC Storm Events Database\"\n        description=\"Storm Data is provided by the National Weather Service (NWS) and contain statistics on personal injuries and damage estimates.\"\n        url=\"https://example.com/dataset/storm-events\"\n        creator=\"NOAA\"\n        distribution={{\n          contentUrl: \"https://www.ncdc.noaa.gov/stormevents/ftp.jsp\",\n          encodingFormat: \"CSV\",\n        }}\n      />\n      {/* Your dataset page content */}\n    </>\n  );\n}\n```\n\n#### Advanced Example with Full Features\n\n```tsx\n<DatasetJsonLd\n  name=\"Global Climate Data 2020-2024\"\n  description=\"Comprehensive climate measurements including temperature, precipitation, and atmospheric data collected from weather stations worldwide\"\n  url=\"https://example.com/datasets/global-climate-2020-2024\"\n  sameAs={[\n    \"https://doi.org/10.1000/182\",\n    \"https://data.gov/dataset/climate-2020-2024\",\n  ]}\n  identifier={[\n    \"https://doi.org/10.1000/182\",\n    {\n      value: \"ark:/12345/fk1234\",\n      propertyID: \"ARK\",\n    },\n  ]}\n  keywords={[\n    \"climate\",\n    \"temperature\",\n    \"precipitation\",\n    \"weather\",\n    \"atmospheric data\",\n  ]}\n  license=\"https://creativecommons.org/publicdomain/zero/1.0/\"\n  isAccessibleForFree={true}\n  creator={[\n    {\n      name: \"National Centers for Environmental Information\",\n      url: \"https://www.ncei.noaa.gov/\",\n      contactPoint: {\n        contactType: \"customer service\",\n        telephone: \"+1-828-271-4800\",\n        email: \"ncei.orders@noaa.gov\",\n      },\n    },\n    \"Dr. Jane Smith\",\n  ]}\n  funder={{\n    name: \"National Science Foundation\",\n    sameAs: \"https://ror.org/021nxhr62\",\n  }}\n  includedInDataCatalog={{\n    name: \"data.gov\",\n    url: \"https://data.gov\",\n  }}\n  distribution={[\n    {\n      contentUrl: \"https://example.com/data/climate-2020-2024.csv\",\n      encodingFormat: \"CSV\",\n      contentSize: \"2.5GB\",\n      description: \"Complete dataset in CSV format\",\n    },\n    {\n      contentUrl: \"https://example.com/data/climate-2020-2024.json\",\n      encodingFormat: \"JSON\",\n      contentSize: \"3.1GB\",\n      description: \"Complete dataset in JSON format\",\n    },\n  ]}\n  temporalCoverage=\"2020-01-01/2024-12-31\"\n  spatialCoverage={{\n    name: \"Global\",\n    geo: {\n      box: \"-90 -180 90 180\",\n    },\n  }}\n  measurementTechnique=\"Satellite observation and ground station measurements\"\n  variableMeasured={[\n    \"temperature\",\n    \"precipitation\",\n    {\n      name: \"Atmospheric Pressure\",\n      value: \"hectopascals\",\n    },\n  ]}\n  version=\"2.1\"\n/>\n```\n\n#### Props\n\n| Property                | Type                                                                       | Description                                                               |\n| ----------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------- |\n| `name`                  | `string`                                                                   | **Required.** A descriptive name of the dataset                           |\n| `description`           | `string`                                                                   | **Required.** A short summary describing the dataset (50-5000 characters) |\n| `url`                   | `string`                                                                   | URL of the dataset landing page                                           |\n| `sameAs`                | `string \\| string[]`                                                       | URLs of pages that unambiguously indicate the dataset's identity          |\n| `identifier`            | `string \\| PropertyValue \\| (string \\| PropertyValue)[]`                   | Identifiers such as DOI or Compact Identifiers                            |\n| `keywords`              | `string \\| string[]`                                                       | Keywords summarizing the dataset                                          |\n| `license`               | `string \\| CreativeWork`                                                   | License under which the dataset is distributed                            |\n| `isAccessibleForFree`   | `boolean`                                                                  | Whether the dataset is accessible without payment                         |\n| `hasPart`               | `Dataset \\| Dataset[]`                                                     | Smaller datasets that are part of this dataset                            |\n| `isPartOf`              | `string \\| Dataset`                                                        | A larger dataset that this dataset is part of                             |\n| `creator`               | `string \\| Person \\| Organization \\| (string \\| Person \\| Organization)[]` | The creator or author of the dataset                                      |\n| `funder`                | `string \\| Person \\| Organization \\| (string \\| Person \\| Organization)[]` | Person or organization that provides financial support                    |\n| `includedInDataCatalog` | `DataCatalog`                                                              | The catalog to which the dataset belongs                                  |\n| `distribution`          | `DataDownload \\| DataDownload[]`                                           | Download locations and formats for the dataset                            |\n| `temporalCoverage`      | `string`                                                                   | Time interval covered by the dataset (ISO 8601 format)                    |\n| `spatialCoverage`       | `string \\| Place`                                                          | Spatial aspect of the dataset (location name or coordinates)              |\n| `alternateName`         | `string \\| string[]`                                                       | Alternative names for the dataset                                         |\n| `citation`              | `string \\| CreativeWork \\| (string \\| CreativeWork)[]`                     | Academic articles to cite alongside the dataset                           |\n| `measurementTechnique`  | `string \\| string[]`                                                       | Technique or methodology used in the dataset                              |\n| `variableMeasured`      | `string \\| PropertyValue \\| (string \\| PropertyValue)[]`                   | Variables that the dataset measures                                       |\n| `version`               | `string \\| number`                                                         | Version number for the dataset                                            |\n| `scriptId`              | `string`                                                                   | Custom ID for the script tag                                              |\n| `scriptKey`             | `string`                                                                   | Custom key for React (defaults to \"dataset-jsonld\")                       |\n\n#### Spatial Coverage Examples\n\n```tsx\n// Named location\nspatialCoverage=\"United States\"\n\n// Point coordinates\nspatialCoverage={{\n  geo: {\n    latitude: 39.3280,\n    longitude: 120.1633,\n  }\n}}\n\n// Bounding box (format: \"minLat minLon maxLat maxLon\")\nspatialCoverage={{\n  geo: {\n    box: \"39.3280 120.1633 40.445 123.7878\",\n  }\n}}\n\n// Circle (format: \"latitude longitude radius\")\nspatialCoverage={{\n  geo: {\n    circle: \"39.3280 120.1633 100\",\n  }\n}}\n```\n\n#### Temporal Coverage Examples\n\n```tsx\n// Single date\ntemporalCoverage = \"2024\";\n\n// Date range\ntemporalCoverage = \"2020-01-01/2024-12-31\";\n\n// Open-ended range\ntemporalCoverage = \"2024-01-01/..\";\n```\n\n#### Best Practices\n\n1. **Comprehensive descriptions**: Provide detailed descriptions (50-5000 characters) that clearly explain what the dataset contains\n2. **Use persistent identifiers**: Include DOIs or other persistent identifiers when available\n3. **Multiple formats**: If your dataset is available in multiple formats, list all distributions\n4. **Specify license**: Always include license information to clarify usage rights\n5. **Include temporal/spatial coverage**: Help users understand the scope of your data\n6. **Use ORCID/ROR IDs**: When specifying creators or funders, use ORCID IDs for individuals and ROR IDs for organizations in the `sameAs` field\n7. **Version your datasets**: Include version numbers to help users track updates\n8. **Link to catalogs**: If your dataset is in a repository like data.gov, include the `includedInDataCatalog` property\n\n> **Note**: Dataset structured data helps your datasets appear in Google's Dataset Search, which is specifically designed for discovering research and government data.\n\n### JobPostingJsonLd\n\nThe `JobPostingJsonLd` component helps you add structured data for job postings to improve their appearance in Google's job search results and the Google Jobs experience.\n\n#### Basic Usage\n\n```tsx\nimport { JobPostingJsonLd } from \"next-seo\";\n\nexport default function JobPage() {\n  return (\n    <>\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>We are looking for a passionate Software Engineer to design, develop and install software solutions.</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        jobLocation=\"Mountain View, CA\"\n        baseSalary={{\n          currency: \"USD\",\n          value: {\n            value: 40.0,\n            unitText: \"HOUR\",\n          },\n        }}\n      />\n      <article>\n        <h1>Software Engineer</h1>\n        {/* Job posting content */}\n      </article>\n    </>\n  );\n}\n```\n\n#### Advanced Example with Full Details\n\n```tsx\n<JobPostingJsonLd\n  title=\"Senior Software Engineer\"\n  description=\"<p>Google is looking for a Senior Software Engineer to join our Cloud team. You will be responsible for designing and developing large-scale distributed systems.</p><p>Requirements:</p><ul><li>5+ years of experience</li><li>Strong knowledge of distributed systems</li><li>Experience with cloud technologies</li></ul>\"\n  datePosted=\"2024-01-18\"\n  validThrough=\"2024-03-18T00:00\"\n  hiringOrganization={{\n    name: \"Google\",\n    sameAs: \"https://www.google.com\",\n    logo: \"https://www.google.com/images/logo.png\",\n  }}\n  jobLocation={{\n    address: {\n      streetAddress: \"1600 Amphitheatre Pkwy\",\n      addressLocality: \"Mountain View\",\n      addressRegion: \"CA\",\n      postalCode: \"94043\",\n      addressCountry: \"US\",\n    },\n  }}\n  url=\"https://careers.google.com/jobs/123456\"\n  employmentType=\"FULL_TIME\"\n  identifier={{\n    name: \"Google\",\n    value: \"1234567\",\n  }}\n  baseSalary={{\n    currency: \"USD\",\n    value: {\n      minValue: 120000,\n      maxValue: 180000,\n      unitText: \"YEAR\",\n    },\n  }}\n  directApply={true}\n  educationRequirements={{\n    credentialCategory: \"bachelor degree\",\n  }}\n  experienceRequirements={{\n    monthsOfExperience: 60,\n  }}\n  experienceInPlaceOfEducation={true}\n/>\n```\n\n#### Remote Job Example\n\n```tsx\n<JobPostingJsonLd\n  title=\"Remote Frontend Developer\"\n  description=\"<p>Join our distributed team as a Frontend Developer. Work from anywhere in the US!</p>\"\n  datePosted=\"2024-01-18\"\n  validThrough=\"2024-02-28T00:00\"\n  hiringOrganization=\"TechStartup Inc.\"\n  jobLocationType=\"TELECOMMUTE\"\n  applicantLocationRequirements={{\n    name: \"USA\",\n  }}\n  employmentType=\"FULL_TIME\"\n  baseSalary={{\n    currency: \"USD\",\n    value: {\n      value: 100000,\n      unitText: \"YEAR\",\n    },\n  }}\n/>\n```\n\n#### Hybrid Job Example (Remote + Office)\n\n```tsx\n<JobPostingJsonLd\n  title=\"Product Manager\"\n  description=\"<p>Hybrid position: work from our NYC office or remotely from NY/NJ/CT.</p>\"\n  datePosted=\"2024-01-18\"\n  hiringOrganization=\"Example Corp\"\n  jobLocation={{\n    address: {\n      streetAddress: \"123 Main St\",\n      addressLocality: \"New York\",\n      addressRegion: \"NY\",\n      postalCode: \"10001\",\n      addressCountry: \"US\",\n    },\n  }}\n  jobLocationType=\"TELECOMMUTE\"\n  applicantLocationRequirements={[\n    { name: \"New York, USA\" },\n    { name: \"New Jersey, USA\" },\n    { name: \"Connecticut, USA\" },\n  ]}\n  employmentType={[\"FULL_TIME\", \"CONTRACTOR\"]}\n/>\n```\n\n#### Props\n\n| Property                        | Type                                                                                             | Description                                                                     |\n| ------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |\n| `title`                         | `string`                                                                                         | **Required.** The title of the job (not the posting). E.g., \"Software Engineer\" |\n| `description`                   | `string`                                                                                         | **Required.** The full job description in HTML format                           |\n| `datePosted`                    | `string`                                                                                         | **Required.** ISO 8601 date when the job was posted                             |\n| `hiringOrganization`            | `string \\| Organization`                                                                         | **Required.** The organization offering the job                                 |\n| `jobLocation`                   | `string \\| Place \\| (string \\| Place)[]`                                                         | Physical location(s) where employee reports to work                             |\n| `url`                           | `string`                                                                                         | The canonical URL for the job posting                                           |\n| `validThrough`                  | `string`                                                                                         | ISO 8601 date when the job posting expires                                      |\n| `employmentType`                | `EmploymentType \\| EmploymentType[]`                                                             | Type of employment (FULL_TIME, PART_TIME, CONTRACTOR, etc.)                     |\n| `identifier`                    | `string \\| PropertyValue`                                                                        | The hiring organization's unique identifier for the job                         |\n| `baseSalary`                    | `MonetaryAmount`                                                                                 | The base salary of the job (as provided by employer)                            |\n| `applicantLocationRequirements` | `Country \\| State \\| (Country \\| State)[]`                                                       | Geographic locations where employees may be located for remote jobs             |\n| `jobLocationType`               | `\"TELECOMMUTE\"`                                                                                  | Set to \"TELECOMMUTE\" for 100% remote jobs                                       |\n| `directApply`                   | `boolean`                                                                                        | Whether the URL enables direct application                                      |\n| `educationRequirements`         | `string \\| EducationalOccupationalCredential \\| (string \\| EducationalOccupationalCredential)[]` | Education requirements for the position                                         |\n| `experienceRequirements`        | `string \\| OccupationalExperienceRequirements`                                                   | Experience requirements for the position                                        |\n| `experienceInPlaceOfEducation`  | `boolean`                                                                                        | Whether experience can substitute for education requirements                    |\n| `scriptId`                      | `string`                                                                                         | Custom ID for the script tag                                                    |\n| `scriptKey`                     | `string`                                                                                         | Custom key for React (defaults to \"jobposting-jsonld\")                          |\n\n#### Employment Type Values\n\nUse these values for the `employmentType` property:\n\n- `FULL_TIME` - Full-time employment\n- `PART_TIME` - Part-time employment\n- `CONTRACTOR` - Contractor position\n- `TEMPORARY` - Temporary employment\n- `INTERN` - Internship position\n- `VOLUNTEER` - Volunteer position\n- `PER_DIEM` - Paid by the day\n- `OTHER` - Other employment type\n\n#### Salary Examples\n\n```tsx\n// Hourly wage\nbaseSalary={{\n  currency: \"USD\",\n  value: {\n    value: 25.00,\n    unitText: \"HOUR\",\n  },\n}}\n\n// Annual salary range\nbaseSalary={{\n  currency: \"USD\",\n  value: {\n    minValue: 80000,\n    maxValue: 120000,\n    unitText: \"YEAR\",\n  },\n}}\n\n// Monthly salary\nbaseSalary={{\n  currency: \"EUR\",\n  value: {\n    value: 5000,\n    unitText: \"MONTH\",\n  },\n}}\n```\n\n#### Best Practices\n\n1. **Complete job descriptions**: Use HTML formatting with `<p>`, `<ul>`, and `<li>` tags for better structure\n2. **Include salary information**: Jobs with salary info get more visibility and engagement\n3. **Set expiration dates**: Always include `validThrough` to automatically expire old postings\n4. **Use employment type**: Specify whether the job is full-time, part-time, contract, etc.\n5. **Remote job requirements**: For remote jobs, always specify `applicantLocationRequirements`\n6. **Direct apply**: Set `directApply: true` if users can apply directly on your site\n7. **Multiple locations**: List all office locations if the job can be performed at multiple sites\n8. **Remove expired jobs**: Update or remove the structured data when jobs are filled\n\n> **Note**: Job postings must comply with Google's content policies. Jobs must be actual openings (not recruiting firms collecting resumes), include application instructions, and be removed when filled.\n\n### DiscussionForumPostingJsonLd\n\nThe `DiscussionForumPostingJsonLd` component helps you add structured data for forum posts and discussions to improve their appearance in Google's Discussions and Forums search feature.\n\n#### Basic Usage\n\n```tsx\nimport { DiscussionForumPostingJsonLd } from \"next-seo\";\n\n<DiscussionForumPostingJsonLd\n  headline=\"I went to the concert!\"\n  text=\"Look at how cool this concert was!\"\n  author=\"Katie Pope\"\n  datePublished=\"2024-01-01T08:00:00+00:00\"\n  url=\"https://example.com/forum/very-popular-thread\"\n  comment={[\n    {\n      text: \"Who's the person you're with?\",\n      author: \"Saul Douglas\",\n      datePublished: \"2024-01-01T09:46:02+00:00\",\n    },\n    {\n      text: \"That's my mom, isn't she cool?\",\n      author: \"Katie Pope\",\n      datePublished: \"2024-01-01T09:50:25+00:00\",\n    },\n  ]}\n/>;\n```\n\n#### Props\n\n| Property               | Type                                                 | Description                                               |\n| ---------------------- | ---------------------------------------------------- | --------------------------------------------------------- |\n| `type`                 | `\"DiscussionForumPosting\" \\| \"SocialMediaPosting\"`   | The type of posting. Defaults to \"DiscussionForumPosting\" |\n| `author`               | `string \\| Person \\| Organization \\| Author[]`       | **Required.** The author(s) of the post                   |\n| `datePublished`        | `string`                                             | **Required.** Publication date in ISO 8601 format         |\n| `text`                 | `string`                                             | The text content of the post                              |\n| `image`                | `string \\| ImageObject \\| (string \\| ImageObject)[]` | Images in the post                                        |\n| `video`                | `VideoObject`                                        | Video content in the post                                 |\n| `headline`             | `string`                                             | The title of the post                                     |\n| `url`                  | `string`                                             | The canonical URL of the discussion                       |\n| `dateModified`         | `string`                                             | Last modification date in ISO 8601 format                 |\n| `comment`              | `Comment[]`                                          | Comments/replies to the post                              |\n| `creativeWorkStatus`   | `string`                                             | Status of the post (e.g., \"Deleted\")                      |\n| `interactionStatistic` | `InteractionCounter \\| InteractionCounter[]`         | User interaction statistics                               |\n| `isPartOf`             | `string \\| CreativeWork`                             | The forum/subforum this post belongs to                   |\n| `sharedContent`        | `string \\| WebPage \\| ImageObject \\| VideoObject`    | Content shared in the post                                |\n| `scriptId`             | `string`                                             | Custom ID for the script tag                              |\n| `scriptKey`            | `string`                                             | React key for the script tag                              |\n\n#### Nested Comments Example\n\n```tsx\n<DiscussionForumPostingJsonLd\n  headline=\"Very Popular Thread\"\n  author={{\n    name: \"Katie Pope\",\n    url: \"https://example.com/user/katie-pope\",\n  }}\n  datePublished=\"2024-01-01T08:00:00+00:00\"\n  text=\"Look at how cool this concert was!\"\n  comment={[\n    {\n      text: \"This should not be this popular\",\n      author: \"Commenter One\",\n      datePublished: \"2024-01-01T09:00:00+00:00\",\n      comment: [\n        {\n          text: \"Yes it should\",\n          author: \"Commenter Two\",\n          datePublished: \"2024-01-01T09:30:00+00:00\",\n        },\n      ],\n    },\n  ]}\n/>\n```\n\n#### Social Media Posting Example\n\n```tsx\n<DiscussionForumPostingJsonLd\n  type=\"SocialMediaPosting\"\n  author=\"SocialUser\"\n  datePublished=\"2024-01-01T12:00:00+00:00\"\n  text=\"Just shared an amazing article!\"\n  sharedContent={{\n    url: \"https://example.com/amazing-article\",\n    name: \"Amazing Article Title\",\n    description: \"A brief description of the article\",\n  }}\n  interactionStatistic={[\n    {\n      interactionType: \"https://schema.org/LikeAction\",\n      userInteractionCount: 150,\n    },\n    {\n      interactionType: \"https://schema.org/ShareAction\",\n      userInteractionCount: 25,\n    },\n  ]}\n/>\n```\n\n#### Interaction Types\n\nThe following interaction types are supported for `interactionStatistic`:\n\n- `https://schema.org/LikeAction` - Likes or upvotes\n- `https://schema.org/DislikeAction` - Dislikes or downvotes\n- `https://schema.org/ViewAction` - View count\n- `https://schema.org/CommentAction` or `https://schema.org/ReplyAction` - Comment count\n- `https://schema.org/ShareAction` - Share count\n\n#### Best Practices\n\n1. **Include all visible content**: Add all text, images, and videos that appear in the post\n2. **Nested comments**: Use the nested structure to represent threaded discussions accurately\n3. **Author information**: Include author URLs linking to profile pages when available\n4. **Interaction statistics**: Add engagement metrics to help Google understand post popularity\n5. **Deleted content**: Use `creativeWorkStatus: \"Deleted\"` for removed posts that remain for context\n6. **Forum hierarchy**: Use `isPartOf` to indicate which subforum or category the post belongs to\n7. **ISO 8601 dates**: Always use proper date formatting with timezone information\n8. **Multi-page threads**: For paginated discussions, set the `url` to the first page\n\n> **Note**: DiscussionForumPosting is designed for forum-style sites where people share first-hand perspectives. For Q&A formats, use Q&A structured data instead.\n\n### EmployerAggregateRatingJsonLd\n\nThe `EmployerAggregateRatingJsonLd` component helps you add structured data for user-generated ratings about hiring organizations. This enables job seekers to see ratings in the enriched job search experience on Google.\n\n#### Basic Usage\n\n```tsx\nimport { EmployerAggregateRatingJsonLd } from \"next-seo\";\n\n<EmployerAggregateRatingJsonLd\n  itemReviewed=\"World's Best Coffee Shop\"\n  ratingValue={91}\n  ratingCount={10561}\n/>;\n```\n\n#### Props\n\n| Property       | Type                     | Description                                                                       |\n| -------------- | ------------------------ | --------------------------------------------------------------------------------- |\n| `itemReviewed` | `string \\| Organization` | **Required.** The organization being rated                                        |\n| `ratingValue`  | `number \\| string`       | **Required.** The rating value (number, fraction, or percentage)                  |\n| `ratingCount`  | `number`                 | The total number of ratings (at least one of ratingCount or reviewCount required) |\n| `reviewCount`  | `number`                 | The number of reviews (at least one of ratingCount or reviewCount required)       |\n| `bestRating`   | `number`                 | The highest value allowed in this rating system (default: 5)                      |\n| `worstRating`  | `number`                 | The lowest value allowed in this rating system (default: 1)                       |\n| `scriptId`     | `string`                 | Custom ID for the script tag                                                      |\n| `scriptKey`    | `string`                 | React key for the script tag                                                      |\n\n#### Advanced Example\n\n```tsx\n<EmployerAggregateRatingJsonLd\n  itemReviewed={{\n    name: \"World's Best Coffee Shop\",\n    sameAs: \"https://www.worlds-best-coffee-shop.example.com\",\n    url: \"https://www.worlds-best-coffee-shop.example.com\",\n    logo: \"https://example.com/logo.png\",\n    address: {\n      streetAddress: \"123 Main St\",\n      addressLocality: \"Seattle\",\n      addressRegion: \"WA\",\n      postalCode: \"98101\",\n      addressCountry: \"US\",\n    },\n  }}\n  ratingValue={91}\n  ratingCount={10561}\n  bestRating={100}\n  worstRating={1}\n/>\n```\n\n#### Custom Rating Scale Example\n\n```tsx\n<EmployerAggregateRatingJsonLd\n  itemReviewed=\"Percentage-Based Company\"\n  ratingValue=\"85%\"\n  reviewCount={250}\n  bestRating={100}\n  worstRating={0}\n/>\n```\n\n#### Best Practices\n\n1. **Always include organization details**: Provide as much information about the organization as possible\n2. **Use sameAs property**: Link to the organization's official website or social media profiles\n3. **Rating scales**: If not using a 5-point scale, always specify bestRating and worstRating\n4. **Count accuracy**: Ensure ratingCount and reviewCount reflect actual user ratings on your site\n5. **Rating derivation**: The ratingValue must be accurately calculated from user ratings\n\n> **Note**: At least one of `ratingCount` or `reviewCount` must be provided. The component will throw an error if neither is present.\n\n> **Looking for job posting structured data?** Check out [JobPostingJsonLd](#jobpostingjsonld) to add complete job listing structured data alongside employer ratings.\n\n### VacationRentalJsonLd\n\nThe `VacationRentalJsonLd` component helps you add structured data for vacation rental listings to improve their appearance in Google Search results. Users can see listing information such as name, description, images, location, rating, and reviews directly in search results.\n\n#### Basic Usage\n\n```tsx\nimport { VacationRentalJsonLd } from \"next-seo\";\n\n<VacationRentalJsonLd\n  containsPlace={{\n    occupancy: {\n      value: 5,\n    },\n  }}\n  identifier=\"abc123\"\n  image=\"https://example.com/vacation-rental-main.jpg\"\n  latitude={42.12345}\n  longitude={101.12345}\n  name=\"Beautiful Beach House\"\n/>;\n```\n\n#### Advanced Usage\n\n```tsx\n<VacationRentalJsonLd\n  containsPlace={{\n    additionalType: \"EntirePlace\",\n    bed: [\n      {\n        numberOfBeds: 1,\n        typeOfBed: \"Queen\",\n      },\n      {\n        numberOfBeds: 2,\n        typeOfBed: \"Single\",\n      },\n    ],\n    occupancy: {\n      value: 5,\n    },\n    amenityFeature: [\n      {\n        name: \"ac\",\n        value: true,\n      },\n      {\n        name: \"wifi\",\n        value: true,\n      },\n      {\n        name: \"poolType\",\n        value: \"Outdoor\",\n      },\n    ],\n    floorSize: {\n      value: 75,\n      unitCode: \"MTK\",\n    },\n    numberOfBathroomsTotal: 2.5,\n    numberOfBedrooms: 3,\n    numberOfRooms: 5,\n    petsAllowed: true,\n    smokingAllowed: false,\n  }}\n  identifier=\"lux-villa-123\"\n  image={[\n    \"https://example.com/image1.jpg\",\n    \"https://example.com/image2.jpg\",\n    \"https://example.com/image3.jpg\",\n    \"https://example.com/image4.jpg\",\n    \"https://example.com/image5.jpg\",\n    \"https://example.com/image6.jpg\",\n    \"https://example.com/image7.jpg\",\n    \"https://example.com/image8.jpg\",\n  ]}\n  latitude={42.12345}\n  longitude={101.12345}\n  name=\"Luxury Ocean View Villa\"\n  additionalType=\"Villa\"\n  address={{\n    addressCountry: \"US\",\n    addressLocality: \"Malibu\",\n    addressRegion: \"California\",\n    postalCode: \"90265\",\n    streetAddress: \"123 Ocean Drive\",\n  }}\n  aggregateRating={{\n    ratingValue: 4.8,\n    ratingCount: 120,\n    reviewCount: 95,\n    bestRating: 5,\n  }}\n  brand={{\n    name: \"Luxury Vacation Rentals Inc\",\n  }}\n  checkinTime=\"15:00:00-08:00\"\n  checkoutTime=\"11:00:00-08:00\"\n  description=\"Stunning beachfront villa with panoramic ocean views\"\n  knowsLanguage={[\"en-US\", \"es-ES\", \"fr-FR\"]}\n  review={[\n    {\n      reviewRating: {\n        ratingValue: 5,\n        bestRating: 5,\n      },\n      author: \"Jane Smith\",\n      datePublished: \"2024-01-15\",\n      contentReferenceTime: \"2024-01-10\",\n    },\n  ]}\n/>\n```\n\n#### Props\n\n| Property                          | Type                             | Description                                                       |\n| --------------------------------- | -------------------------------- | ----------------------------------------------------------------- |\n| **containsPlace**                 | `Accommodation`                  | **Required.** Details about the accommodation including occupancy |\n| **containsPlace.occupancy**       | `QuantitativeValue`              | **Required.** Maximum number of guests                            |\n| **containsPlace.occupancy.value** | `number`                         | **Required.** The numerical value of guests                       |\n| **identifier**                    | `string`                         | **Required.** A unique identifier for the property                |\n| **image**                         | `string \\| ImageObject \\| array` | **Required.** Minimum 8 photos (bedroom, bathroom, common area)   |\n| **latitude**                      | `number \\| string`               | **Required.** Latitude with 5 decimal precision                   |\n| **longitude**                     | `number \\| string`               | **Required.** Longitude with 5 decimal precision                  |\n| **name**                          | `string`                         | **Required.** The name of the vacation rental                     |\n| `additionalType`                  | `string`                         | Type of rental (e.g., House, Villa, Apartment, Cottage)           |\n| `address`                         | `PostalAddress`                  | Full physical address of the rental                               |\n| `aggregateRating`                 | `AggregateRating`                | Average rating based on multiple reviews                          |\n| `brand`                           | `Brand`                          | The brand associated with the property                            |\n| `checkinTime`                     | `string`                         | Earliest check-in time in ISO 8601 format                         |\n| `checkoutTime`                    | `string`                         | Latest check-out time in ISO 8601 format                          |\n| `description`                     | `string`                         | A description of the property                                     |\n| `knowsLanguage`                   | `string \\| string[]`             | Languages the host speaks (IETF BCP 47)                           |\n| `review`                          | `Review \\| Review[]`             | User reviews of the listing                                       |\n| `geo`                             | `object`                         | Alternative way to specify coordinates                            |\n| `scriptId`                        | `string`                         | Custom ID for the script tag                                      |\n| `scriptKey`                       | `string`                         | Custom data-seo attribute value                                   |\n\n#### Accommodation Properties\n\n| Property                 | Type                                    | Description                                               |\n| ------------------------ | --------------------------------------- | --------------------------------------------------------- |\n| `additionalType`         | `string`                                | Type of room (EntirePlace, PrivateRoom, SharedRoom)       |\n| `bed`                    | `BedDetails \\| BedDetails[]`            | Information about beds                                    |\n| `amenityFeature`         | `LocationFeatureSpecification \\| array` | Property amenities                                        |\n| `floorSize`              | `QuantitativeValue`                     | Size with unitCode (FTK/SQFT for sq ft, MTK/SQM for sq m) |\n| `numberOfBathroomsTotal` | `number`                                | Total bathrooms (can be decimal, e.g., 2.5)               |\n| `numberOfBedrooms`       | `number`                                | Total number of bedrooms                                  |\n| `numberOfRooms`          | `number`                                | Total number of rooms                                     |\n| `petsAllowed`            | `boolean`                               | Whether pets are allowed                                  |\n| `smokingAllowed`         | `boolean`                               | Whether smoking is allowed                                |\n\n#### Amenity Feature Values\n\n**Boolean amenities** (use `value: true/false`):\n\n- `ac`, `airportShuttle`, `balcony`, `beachAccess`, `childFriendly`, `crib`, `elevator`, `fireplace`, `freeBreakfast`, `gymFitnessEquipment`, `heating`, `hotTub`, `instantBookable`, `ironingBoard`, `kitchen`, `microwave`, `outdoorGrill`, `ovenStove`, `patio`, `petsAllowed`, `pool`, `privateBeachAccess`, `selfCheckinCheckout`, `smokingAllowed`, `tv`, `washerDryer`, `wheelchairAccessible`, `wifi`\n\n**Non-boolean amenities** (use `value: \"string\"`):\n\n- `internetType`: \"Free\", \"Paid\", \"None\"\n- `parkingType`: \"Free\", \"Paid\", \"None\"\n- `poolType`: \"Indoor\", \"Outdoor\", \"None\"\n- `licenseNum`: License number with authority context\n\n#### Best Practices\n\n1. **Minimum 8 images**: Include at least one photo of bedroom, bathroom, and common areas\n2. **Precise coordinates**: Use at least 5 decimal places for latitude/longitude\n3. **Complete address**: Provide full physical address including unit numbers\n4. **Accurate occupancy**: Specify the maximum number of guests allowed\n5. **Languages**: List all languages the host can communicate in\n6. **Reviews**: Include the `contentReferenceTime` for French listings\n7. **Unique identifier**: Use a stable ID that won't change with content updates\n\n> **Note**: This feature requires integration with Google's Hotel Center and is limited to sites that meet eligibility criteria. Visit the [vacation rental interest form](https://support.google.com/hotelprices/contact/vacation_rentals_interest) to get started.\n\n### VideoJsonLd\n\nThe `VideoJsonLd` component helps you add structured data for videos to improve their appearance in Google Search results. This includes standard video results, video carousels, and rich video previews. You can also mark live videos with a LIVE badge, add key moments for video navigation, and specify viewing restrictions.\n\n#### Basic Usage\n\n```tsx\nimport { VideoJsonLd } from \"next-seo\";\n\n<VideoJsonLd\n  name=\"How to Make a Perfect Cake\"\n  description=\"Learn how to make the perfect chocolate cake with this easy recipe\"\n  thumbnailUrl=\"https://example.com/cake-video-thumbnail.jpg\"\n  uploadDate=\"2024-01-15T08:00:00+00:00\"\n  contentUrl=\"https://example.com/videos/cake-recipe.mp4\"\n  embedUrl=\"https://example.com/embed/cake-recipe\"\n  duration=\"PT10M30S\"\n/>;\n```\n\n#### Advanced Usage with All Features\n\n```tsx\n<VideoJsonLd\n  name=\"How to Make a Perfect Cake\"\n  description=\"Learn how to make the perfect chocolate cake with this easy recipe\"\n  thumbnailUrl={[\n    \"https://example.com/thumbnails/1x1/cake.jpg\",\n    \"https://example.com/thumbnails/4x3/cake.jpg\",\n    \"https://example.com/thumbnails/16x9/cake.jpg\",\n  ]}\n  uploadDate=\"2024-01-15T08:00:00+00:00\"\n  contentUrl=\"https://example.com/videos/cake-recipe.mp4\"\n  embedUrl=\"https://example.com/embed/cake-recipe\"\n  duration=\"PT10M30S\"\n  expires=\"2025-01-15T00:00:00+00:00\"\n  interactionStatistic={{\n    interactionType: \"WatchAction\",\n    userInteractionCount: 150000,\n  }}\n  regionsAllowed={[\"US\", \"CA\", \"GB\"]}\n  author=\"Chef Julia\"\n  publisher={{\n    name: \"Cooking Channel\",\n    logo: \"https://example.com/cooking-channel-logo.png\",\n  }}\n/>\n```\n\n#### Live Video with LIVE Badge\n\n```tsx\n<VideoJsonLd\n  name=\"Live Cooking Show: Holiday Special\"\n  description=\"Join us for a live cooking demonstration of holiday favorites\"\n  thumbnailUrl=\"https://example.com/live-show-thumbnail.jpg\"\n  uploadDate=\"2024-12-20T10:00:00+00:00\"\n  embedUrl=\"https://example.com/live/holiday-special\"\n  publication={{\n    isLiveBroadcast: true,\n    startDate: \"2024-12-25T18:00:00+00:00\",\n    endDate: \"2024-12-25T20:00:00+00:00\",\n  }}\n/>\n```\n\n#### Video with Key Moments (Clips)\n\n```tsx\n<VideoJsonLd\n  name=\"Complete Cake Baking Tutorial\"\n  description=\"A comprehensive guide to baking cakes from scratch\"\n  thumbnailUrl=\"https://example.com/tutorial-thumbnail.jpg\"\n  uploadDate=\"2024-01-15T08:00:00+00:00\"\n  contentUrl=\"https://example.com/videos/complete-tutorial.mp4\"\n  duration=\"PT30M\"\n  hasPart={[\n    {\n      name: \"Gathering Ingredients\",\n      startOffset: 0,\n      endOffset: 120,\n      url: \"https://example.com/videos/complete-tutorial?t=0\",\n    },\n    {\n      name: \"Mixing the Batter\",\n      startOffset: 120,\n      endOffset: 480,\n      url: \"https://example.com/videos/complete-tutorial?t=120\",\n    },\n    {\n      name: \"Baking and Decorating\",\n      startOffset: 480,\n      endOffset: 1800,\n      url: \"https://example.com/videos/complete-tutorial?t=480\",\n    },\n  ]}\n/>\n```\n\n#### Video with Automatic Key Moments (SeekToAction)\n\n```tsx\n<VideoJsonLd\n  name=\"Recipe Collection Video\"\n  description=\"Multiple recipes in one convenient video\"\n  thumbnailUrl=\"https://example.com/collection-thumbnail.jpg\"\n  uploadDate=\"2024-01-15T08:00:00+00:00\"\n  embedUrl=\"https://example.com/embed/recipe-collection\"\n  potentialAction={{\n    target: \"https://example.com/videos/collection?t={seek_to_second_number}\",\n    \"startOffset-input\": \"required name=seek_to_second_number\",\n  }}\n/>\n```\n\n#### Props\n\n| Property               | Type                                        | Description                                                                                       |\n| ---------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------- |\n| `name`                 | `string`                                    | **Required.** The title of the video                                                              |\n| `description`          | `string`                                    | **Required.** A description of the video                                                          |\n| `thumbnailUrl`         | `string \\| ImageObject \\| array`            | **Required.** URLs or ImageObjects for video thumbnails. Google recommends multiple aspect ratios |\n| `uploadDate`           | `string`                                    | **Required.** The date and time the video was published in ISO 8601 format                        |\n| `contentUrl`           | `string`                                    | Direct URL to the video file. Recommended if available                                            |\n| `embedUrl`             | `string`                                    | URL to the embedded video player. Use if contentUrl isn't available                               |\n| `duration`             | `string`                                    | Video duration in ISO 8601 format (e.g., \"PT30M\" for 30 minutes)                                  |\n| `expires`              | `string`                                    | Date after which the video is no longer available                                                 |\n| `interactionStatistic` | `InteractionCounter \\| array`               | View counts, likes, or other interaction metrics                                                  |\n| `regionsAllowed`       | `string \\| string[]`                        | Countries where the video is viewable (ISO 3166 format)                                           |\n| `ineligibleRegion`     | `string \\| string[]`                        | Countries where the video is blocked                                                              |\n| `publication`          | `BroadcastEvent \\| array`                   | For live videos - includes broadcast times and live status                                        |\n| `hasPart`              | `Clip \\| Clip[]`                            | Video segments/chapters with timestamps and labels                                                |\n| `potentialAction`      | `SeekToAction`                              | URL pattern for automatic key moments                                                             |\n| `author`               | `string \\| Person \\| Organization \\| array` | Video creator(s)                                                                                  |\n| `publisher`            | `Organization`                              | Organization that published the video                                                             |\n| `type`                 | `\"VideoObject\"`                             | Schema type. Defaults to \"VideoObject\"                                                            |\n| `scriptId`             | `string`                                    | Custom ID for the script tag                                                                      |\n| `scriptKey`            | `string`                                    | Custom key for React                                                                              |\n\n#### Best Practices\n\n1. **Thumbnail Images**: Provide multiple thumbnail URLs in different aspect ratios (16:9, 4:3, 1:1) for optimal display\n2. **Duration Format**: Use ISO 8601 duration format: PT[hours]H[minutes]M[seconds]S\n3. **Content vs Embed URL**:\n   - Use `contentUrl` for direct video files (mp4, webm, etc.)\n   - Use `embedUrl` for video player pages\n   - Provide both when possible\n4. **Live Videos**: For live streams, always include `publication` with `isLiveBroadcast: true`\n5. **Key Moments**:\n   - Use `hasPart` with `Clip` objects when you want to specify exact timestamps\n   - Use `potentialAction` with `SeekToAction` to let Google automatically detect key moments\n6. **Timestamps**: Always use ISO 8601 format with timezone information\n7. **Region Restrictions**: Use two-letter ISO 3166-1 country codes\n\n### ProfilePageJsonLd\n\nThe `ProfilePageJsonLd` component helps you add structured data for profile pages where creators (either people or organizations) share first-hand perspectives. This helps Google Search understand the creators in an online community and show better content from that community in search results, including the Discussions and Forums feature.\n\n#### Basic Usage\n\n```tsx\nimport { ProfilePageJsonLd } from \"next-seo\";\n\n<ProfilePageJsonLd\n  mainEntity=\"Angelo Huff\"\n  dateCreated=\"2024-12-23T12:34:00-05:00\"\n  dateModified=\"2024-12-26T14:53:00-05:00\"\n/>;\n```\n\n#### Advanced Usage\n\n```tsx\n<ProfilePageJsonLd\n  mainEntity={{\n    name: \"Angelo Huff\",\n    alternateName: \"ahuff23\",\n    identifier: \"123475623\",\n    description: \"Defender of Truth\",\n    image: \"https://example.com/avatars/ahuff23.jpg\",\n    sameAs: [\n      \"https://www.example.com/real-angelo\",\n      \"https://example.com/profile/therealangelohuff\",\n    ],\n    interactionStatistic: [\n      {\n        interactionType: \"https://schema.org/FollowAction\",\n        userInteractionCount: 1,\n      },\n      {\n        interactionType: \"https://schema.org/LikeAction\",\n        userInteractionCount: 5,\n      },\n    ],\n    agentInteractionStatistic: {\n      interactionType: \"https://schema.org/WriteAction\",\n      userInteractionCount: 2346,\n    },\n  }}\n  dateCreated=\"2024-12-23T12:34:00-05:00\"\n  dateModified=\"2024-12-26T14:53:00-05:00\"\n/>\n```\n\n#### Organization Profile Example\n\n```tsx\n<ProfilePageJsonLd\n  mainEntity={{\n    \"@type\": \"Organization\",\n    name: \"ACME Corporation\",\n    url: \"https://acme.com\",\n    logo: \"https://acme.com/logo.png\",\n    sameAs: [\"https://twitter.com/acme\", \"https://linkedin.com/company/acme\"],\n    interactionStatistic: {\n      interactionType: \"https://schema.org/FollowAction\",\n      userInteractionCount: 15000,\n    },\n  }}\n/>\n```\n\n#### Props\n\n| Property       | Type                                                                                       | Description                                                         |\n| -------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- |\n| `mainEntity`   | `string \\| Person \\| Organization \\| Omit<Person, \"@type\"> \\| Omit<Organization, \"@type\">` | **Required.** The person or organization this profile page is about |\n| `dateCreated`  | `string`                                                                                   | Date and time the profile was created (ISO 8601 format)             |\n| `dateModified` | `string`                                                                                   | Date and time the profile was last modified (ISO 8601 format)       |\n| `scriptId`     | `string`                                                                                   | Custom ID for the script tag                                        |\n| `scriptKey`    | `string`                                                                                   | Custom key for React reconciliation                                 |\n\n#### Person/Organization Properties\n\nWhen providing an object for `mainEntity`, you can include these properties:\n\n**Common Properties:**\n\n- `name`: The primary name (real name preferred)\n- `alternateName`: Alternate identifier (e.g., username)\n- `identifier`: Unique ID within your site\n- `description`: User's byline or credential\n- `image`: Profile image URL\n- `sameAs`: Array of external profile URLs\n- `interactionStatistic`: User statistics (followers, likes, etc.)\n- `agentInteractionStatistic`: User's own activity statistics\n\n**Interaction Types:**\n\n- `https://schema.org/FollowAction`: Number of followers/following\n- `https://schema.org/LikeAction`: Number of likes\n- `https://schema.org/WriteAction`: Number of posts\n- `https://schema.org/ShareAction`: Number of reshares\n- `https://schema.org/BefriendAction`: Bi-directional relationships\n\n#### Best Practices\n\n1. **Profile focus**: The page must primarily focus on a single person or organization\n2. **Real names**: Use `name` for real names and `alternateName` for usernames\n3. **Stable identifiers**: Use IDs that won't change even if usernames change\n4. **Multiple images**: Include profile images in multiple aspect ratios (16x9, 4x3, 1x1)\n5. **ISO 8601 dates**: Always include timezone information in dates\n6. **Platform statistics**: Only include stats from the current platform\n\n#### Valid Use Cases\n\n✅ User profile pages on forums or social media sites\n✅ Author pages on news sites\n✅ \"About Me\" pages on blog sites\n✅ Employee pages on company websites\n\n#### Invalid Use Cases\n\n❌ Main home page of a store\n❌ Organization review sites (where the org isn't affiliated with the site)\n\n> **Note**: ProfilePage markup is designed for sites where creators share first-hand perspectives. It can be linked from Article and Recipe structured data authors, and is often used in discussion forum and Q&A page structured data.\n\n### SoftwareApplicationJsonLd\n\nThe `SoftwareApplicationJsonLd` component helps you add structured data for software applications, including mobile apps, web apps, desktop software, and games. This can help your app appear in rich results and improve its visibility in app-related searches.\n\n#### Basic Usage (Free App)\n\n```tsx\nimport { SoftwareApplicationJsonLd } from \"next-seo\";\n\n<SoftwareApplicationJsonLd\n  name=\"My Awesome App\"\n  offers={{\n    price: 0,\n    priceCurrency: \"USD\",\n  }}\n  aggregateRating={{\n    ratingValue: 4.5,\n    ratingCount: 100,\n  }}\n/>;\n```\n\n#### Paid App Example\n\n```tsx\n<SoftwareApplicationJsonLd\n  name=\"Premium Photo Editor\"\n  applicationCategory=\"DesignApplication\"\n  operatingSystem=\"iOS 14.0+\"\n  offers={{\n    price: 9.99,\n    priceCurrency: \"USD\",\n  }}\n  aggregateRating={{\n    ratingValue: 4.8,\n    reviewCount: 2500,\n  }}\n  description=\"Professional photo editing on the go\"\n  screenshot={[\n    \"https://example.com/screenshot1.jpg\",\n    \"https://example.com/screenshot2.jpg\",\n  ]}\n/>\n```\n\n#### Mobile Application\n\n```tsx\n<SoftwareApplicationJsonLd\n  type=\"MobileApplication\"\n  name=\"Fitness Tracker Pro\"\n  applicationCategory=\"HealthApplication\"\n  operatingSystem=\"Android 8.0+, iOS 12.0+\"\n  offers={{\n    price: 0,\n    priceCurrency: \"USD\",\n  }}\n  review={[\n    {\n      author: \"Jane Smith\",\n      reviewRating: { ratingValue: 5 },\n      reviewBody: \"Best fitness app I've ever used!\",\n    },\n  ]}\n  permissions={[\"location\", \"camera\", \"storage\"]}\n  featureList={[\n    \"GPS tracking\",\n    \"Heart rate monitoring\",\n    \"Social challenges\",\n    \"Meal planning\",\n  ]}\n/>\n```\n\n#### Web Application\n\n```tsx\n<SoftwareApplicationJsonLd\n  type=\"WebApplication\"\n  name=\"Project Management Suite\"\n  url=\"https://app.example.com\"\n  applicationCategory=\"BusinessApplication\"\n  applicationSubCategory=\"ProjectManagement\"\n  offers={[\n    {\n      price: 0,\n      priceCurrency: \"USD\",\n      availability: \"https://schema.org/InStock\",\n    },\n    {\n      price: 29.99,\n      priceCurrency: \"USD\",\n      availability: \"https://schema.org/InStock\",\n    },\n  ]}\n  aggregateRating={{\n    ratingValue: 4.7,\n    ratingCount: 5000,\n  }}\n  screenshot={{\n    url: \"https://example.com/app-dashboard.jpg\",\n    caption: \"Main dashboard view\",\n  }}\n/>\n```\n\n#### Video Game (Co-typed)\n\nFor video games, Google requires co-typing with another application type:\n\n```tsx\n<SoftwareApplicationJsonLd\n  type={[\"VideoGame\", \"MobileApplication\"]}\n  name=\"Epic Adventure Quest\"\n  applicationCategory=\"GameApplication\"\n  operatingSystem=\"iOS 13.0+, Android 9.0+\"\n  offers={{\n    price: 4.99,\n    priceCurrency: \"USD\",\n  }}\n  aggregateRating={{\n    ratingValue: 4.6,\n    ratingCount: 10000,\n  }}\n  contentRating=\"Everyone 10+\"\n  screenshot={[\n    \"https://example.com/gameplay1.jpg\",\n    \"https://example.com/gameplay2.jpg\",\n  ]}\n  featureList={[\n    \"Multiplayer battles\",\n    \"50+ hours of gameplay\",\n    \"Cloud save support\",\n  ]}\n/>\n```\n\n#### Props\n\n| Property                | Type                                                 | Description                                                     |\n| ----------------------- | ---------------------------------------------------- | --------------------------------------------------------------- |\n| `name`                  | `string`                                             | **Required**. The name of the software application              |\n| `type`                  | `ApplicationType \\| VideoGameCoTyped`                | Type of application. Defaults to \"SoftwareApplication\"          |\n| `offers`                | `Offer \\| Offer[]`                                   | **Required**. Pricing information. Set price to 0 for free apps |\n| `aggregateRating`       | `AggregateRating`                                    | **Required** (or use `review`). Average rating information      |\n| `review`                | `Review \\| Review[]`                                 | **Required** (or use `aggregateRating`). Individual reviews     |\n| `applicationCategory`   | `string`                                             | **Recommended**. Category of the app (e.g., \"GameApplication\")  |\n| `operatingSystem`       | `string`                                             | **Recommended**. Required OS (e.g., \"Windows 10, macOS 10.15+\") |\n| `description`           | `string`                                             | Description of the application                                  |\n| `url`                   | `string`                                             | URL of the app's webpage                                        |\n| `image`                 | `string \\| ImageObject \\| (string \\| ImageObject)[]` | App icon or logo                                                |\n| `screenshot`            | `string \\| ImageObject \\| (string \\| ImageObject)[]` | Screenshots of the app                                          |\n| `featureList`           | `string \\| string[]`                                 | Key features of the app                                         |\n| `permissions`           | `string \\| string[]`                                 | Required permissions                                            |\n| `softwareVersion`       | `string`                                             | Current version number                                          |\n| `datePublished`         | `string`                                             | Initial release date                                            |\n| `dateModified`          | `string`                                             | Last update date                                                |\n| `author`                | `string \\| Person \\| Organization`                   | Developer or development team                                   |\n| `publisher`             | `Organization`                                       | Publishing organization                                         |\n| `downloadUrl`           | `string`                                             | Direct download link                                            |\n| `installUrl`            | `string`                                             | Installation link                                               |\n| `memoryRequirements`    | `string`                                             | RAM requirements                                                |\n| `storageRequirements`   | `string`                                             | Storage space needed                                            |\n| `processorRequirements` | `string`                                             | CPU requirements                                                |\n| `countriesSupported`    | `string \\| string[]`                                 | Supported countries                                             |\n| `applicationSuite`      | `string`                                             | Suite the app belongs to                                        |\n\n#### Application Types\n\nThe component supports all Google-recognized application types:\n\n- `SoftwareApplication` (default)\n- `MobileApplication`\n- `WebApplication`\n- `GameApplication`\n- `SocialNetworkingApplication`\n- `TravelApplication`\n- `ShoppingApplication`\n- `SportsApplication`\n- `LifestyleApplication`\n- `BusinessApplication`\n- `DesignApplication`\n- `DeveloperApplication`\n- `DriverApplication`\n- `EducationalApplication`\n- `HealthApplication`\n- `FinanceApplication`\n- `SecurityApplication`\n- `BrowserApplication`\n- `CommunicationApplication`\n- `DesktopEnhancementApplication`\n- `EntertainmentApplication`\n- `MultimediaApplication`\n- `HomeApplication`\n- `UtilitiesApplication`\n- `ReferenceApplication`\n\n#### Best Practices\n\n1. **Always include pricing**: Use `offers` with `price: 0` for free apps\n2. **Provide ratings or reviews**: Include either `aggregateRating` or `review` (required by Google)\n3. **Specify OS requirements**: Use `operatingSystem` for better user experience\n4. **Multiple screenshots**: Include various screenshots showing key features\n5. **Video games**: Always co-type with another application type (e.g., `[\"VideoGame\", \"MobileApplication\"]`)\n6. **Feature lists**: Highlight key features that differentiate your app\n7. **Version information**: Keep `softwareVersion` and `dateModified` current\n8. **Permissions transparency**: List all required permissions for mobile apps\n\n### ProductJsonLd\n\nThe `ProductJsonLd` component helps you add structured data for products to improve their appearance in search results. Products can appear as rich snippets with ratings, prices, and availability information.\n\n#### Basic Usage\n\n```tsx\nimport { ProductJsonLd } from \"next-seo\";\n\n<ProductJsonLd\n  name=\"Executive Anvil\"\n  description=\"Sleeker than ACME's Classic Anvil, perfect for the business traveler\"\n  image=\"https://example.com/products/anvil.jpg\"\n  offers={{\n    price: 119.99,\n    priceCurrency: \"USD\",\n    availability: \"InStock\",\n  }}\n/>;\n```\n\n#### Product with Reviews\n\n```tsx\n<ProductJsonLd\n  name=\"Executive Anvil\"\n  sku=\"0446310786\"\n  mpn=\"925872\"\n  brand=\"ACME\"\n  review={{\n    reviewRating: {\n      ratingValue: 4.5,\n      bestRating: 5,\n    },\n    author: \"Fred Benson\",\n    reviewBody:\n      \"This anvil has held up well after many uses. Highly recommended!\",\n  }}\n  aggregateRating={{\n    ratingValue: 4.4,\n    reviewCount: 89,\n  }}\n  offers={{\n    price: 119.99,\n    priceCurrency: \"USD\",\n    availability: \"InStock\",\n    priceValidUntil: \"2024-12-31\",\n  }}\n/>\n```\n\n#### Product with Pros and Cons\n\n```tsx\n<ProductJsonLd\n  name=\"Cheese Grater Pro\"\n  review={{\n    name: \"Cheese Grater Pro Review\",\n    author: \"Pascal Van Cleeff\",\n    reviewRating: {\n      ratingValue: 4,\n      bestRating: 5,\n    },\n    positiveNotes: {\n      itemListElement: [\n        { name: \"Consistent results\" },\n        { name: \"Still sharp after many uses\" },\n        { name: \"Easy to clean\" },\n      ],\n    },\n    negativeNotes: {\n      itemListElement: [\n        { name: \"No child protection\" },\n        { name: \"Lacking advanced features\" },\n      ],\n    },\n  }}\n  offers={{\n    price: 29.99,\n    priceCurrency: \"USD\",\n  }}\n/>\n```\n\n#### Shopping Aggregator (Multiple Sellers)\n\n```tsx\n<ProductJsonLd\n  name=\"Executive Anvil\"\n  image={[\n    \"https://example.com/photos/1x1/photo.jpg\",\n    \"https://example.com/photos/4x3/photo.jpg\",\n    \"https://example.com/photos/16x9/photo.jpg\",\n  ]}\n  description=\"Sleeker than ACME's Classic Anvil\"\n  sku=\"0446310786\"\n  mpn=\"925872\"\n  brand=\"ACME\"\n  offers={{\n    lowPrice: 119.99,\n    highPrice: 199.99,\n    priceCurrency: \"USD\",\n    offerCount: 5,\n  }}\n  aggregateRating={{\n    ratingValue: 4.4,\n    reviewCount: 89,\n  }}\n/>\n```\n\n#### Complete Example with All Features\n\n```tsx\n<ProductJsonLd\n  name=\"Executive Anvil\"\n  description=\"Sleeker than ACME's Classic Anvil\"\n  url=\"https://example.com/products/anvil\"\n  sku=\"0446310786\"\n  mpn=\"925872\"\n  gtin13=\"0614141999996\"\n  brand=\"ACME\"\n  category=\"Hardware\"\n  color=\"Silver\"\n  material=\"Steel\"\n  model=\"EA-2024\"\n  productID=\"anvil-001\"\n  weight={{\n    value: 10,\n    unitCode: \"KGM\",\n  }}\n  width=\"30cm\"\n  height=\"20cm\"\n  depth=\"15cm\"\n  manufacturer=\"ACME Manufacturing\"\n  releaseDate=\"2024-01-01\"\n  award=\"Best Anvil 2024\"\n  image={[\n    \"https://example.com/photos/1x1/photo.jpg\",\n    \"https://example.com/photos/4x3/photo.jpg\",\n    \"https://example.com/photos/16x9/photo.jpg\",\n  ]}\n  review={[\n    {\n      reviewRating: { ratingValue: 5, bestRating: 5 },\n      author: \"Alice Johnson\",\n      reviewBody: \"Excellent quality!\",\n    },\n    {\n      reviewRating: { ratingValue: 4, bestRating: 5 },\n      author: \"Bob Smith\",\n      reviewBody: \"Good product, fast shipping.\",\n    },\n  ]}\n  aggregateRating={{\n    ratingValue: 4.4,\n    reviewCount: 89,\n  }}\n  offers={{\n    price: 119.99,\n    priceCurrency: \"USD\",\n    availability: \"InStock\",\n    priceValidUntil: \"2024-12-31\",\n    url: \"https://example.com/buy/anvil\",\n    seller: {\n      name: \"ACME Store\",\n      url: \"https://example.com\",\n    },\n  }}\n/>\n```\n\n#### Props\n\n| Property             | Type                                               | Description                              |\n| -------------------- | -------------------------------------------------- | ---------------------------------------- |\n| `name`               | `string`                                           | **Required.** Product name               |\n| `description`        | `string`                                           | Product description                      |\n| `image`              | `string \\| ImageObject \\| Array`                   | Product images                           |\n| `sku`                | `string`                                           | Stock Keeping Unit                       |\n| `mpn`                | `string`                                           | Manufacturer Part Number                 |\n| `gtin`               | `string`                                           | Global Trade Item Number                 |\n| `gtin8`              | `string`                                           | 8-digit GTIN                             |\n| `gtin12`             | `string`                                           | 12-digit GTIN (UPC)                      |\n| `gtin13`             | `string`                                           | 13-digit GTIN (EAN)                      |\n| `gtin14`             | `string`                                           | 14-digit GTIN                            |\n| `brand`              | `string \\| Brand`                                  | Product brand                            |\n| `review`             | `ProductReview \\| ProductReview[]`                 | Product reviews                          |\n| `aggregateRating`    | `AggregateRating`                                  | Aggregate rating from all reviews        |\n| `offers`             | `ProductOffer \\| AggregateOffer \\| ProductOffer[]` | Price and availability (**recommended**) |\n| `category`           | `string`                                           | Product category                         |\n| `color`              | `string`                                           | Product color                            |\n| `material`           | `string`                                           | Product material                         |\n| `model`              | `string`                                           | Product model                            |\n| `productID`          | `string`                                           | Product identifier                       |\n| `url`                | `string`                                           | Product page URL                         |\n| `weight`             | `string \\| QuantitativeValue`                      | Product weight                           |\n| `width`              | `string \\| QuantitativeValue`                      | Product width                            |\n| `height`             | `string \\| QuantitativeValue`                      | Product height                           |\n| `depth`              | `string \\| QuantitativeValue`                      | Product depth                            |\n| `additionalProperty` | `PropertyValue[]`                                  | Additional product properties            |\n| `manufacturer`       | `string \\| Organization \\| Person`                 | Product manufacturer                     |\n| `releaseDate`        | `string`                                           | Product release date                     |\n| `productionDate`     | `string`                                           | Production date                          |\n| `purchaseDate`       | `string`                                           | Purchase date                            |\n| `expirationDate`     | `string`                                           | Expiration date                          |\n| `award`              | `string \\| string[]`                               | Awards received                          |\n| `isCar`              | `boolean`                                          | Set to true for car products             |\n\n#### Important Requirements\n\nGoogle requires at least one of the following properties for product snippets:\n\n- `review` - A nested review of the product\n- `aggregateRating` - The overall rating based on multiple reviews\n- `offers` - Price and availability information\n\n#### Offer Properties\n\n| Property          | Type                     | Description                              |\n| ----------------- | ------------------------ | ---------------------------------------- |\n| `price`           | `number \\| string`       | Product price                            |\n| `priceCurrency`   | `string`                 | Currency code (e.g., \"USD\")              |\n| `availability`    | `ItemAvailability`       | Availability status                      |\n| `priceValidUntil` | `string`                 | Date until price is valid                |\n| `url`             | `string`                 | URL to purchase product                  |\n| `seller`          | `Organization \\| Person` | Seller information                       |\n| `itemCondition`   | `string`                 | Condition (New, Used, Refurbished, etc.) |\n\n#### AggregateOffer Properties (Multiple Sellers)\n\n| Property        | Type               | Description                 |\n| --------------- | ------------------ | --------------------------- |\n| `lowPrice`      | `number \\| string` | **Required.** Lowest price  |\n| `priceCurrency` | `string`           | **Required.** Currency code |\n| `highPrice`     | `number \\| string` | Highest price               |\n| `offerCount`    | `number`           | Number of offers            |\n\n#### Best Practices\n\n1. **Always include one of**: review, aggregateRating, or offers (Google requirement)\n2. **Multiple images**: Provide images in different aspect ratios (1x1, 4x3, 16x9)\n3. **Use specific identifiers**: Include SKU, MPN, or GTIN when available\n4. **Pros and cons**: Use positiveNotes and negativeNotes for editorial reviews\n5. **Price information**: Always include priceCurrency with price\n6. **Availability**: Use schema.org values (InStock, OutOfStock, PreOrder, etc.)\n7. **Multiple sellers**: Use AggregateOffer for shopping comparison sites\n8. **Car products**: Set `isCar={true}` for automotive products to add Car type\n\n### ProductGroup (Product Variants)\n\nThe `ProductJsonLd` component now supports ProductGroup for representing product variants (different sizes, colors, materials, etc.) of the same product. This helps Google understand product variations and can enable variant displays in search results.\n\n#### Single-Page Variant Example\n\nUse this approach when all variants are selectable on a single product page:\n\n```tsx\nimport { ProductJsonLd } from \"next-seo\";\n\n<ProductJsonLd\n  type=\"ProductGroup\"\n  name=\"Wool Winter Coat\"\n  description=\"Premium wool coat available in multiple colors and sizes\"\n  productGroupID=\"WC2024\"\n  brand=\"Nordic Style\"\n  variesBy={[\"size\", \"color\"]}\n  aggregateRating={{\n    ratingValue: 4.6,\n    reviewCount: 127,\n  }}\n  hasVariant={[\n    {\n      name: \"Wool Winter Coat - Small Green\",\n      sku: \"WC2024-S-GRN\",\n      size: \"small\",\n      color: \"Green\",\n      offers: {\n        price: 119.99,\n        priceCurrency: \"USD\",\n        availability: \"InStock\",\n        url: \"https://example.com/coat?size=small&color=green\",\n      },\n    },\n    {\n      name: \"Wool Winter Coat - Large Blue\",\n      sku: \"WC2024-L-BLU\",\n      size: \"large\",\n      color: \"Blue\",\n      offers: {\n        price: 139.99,\n        priceCurrency: \"USD\",\n        availability: \"BackOrder\",\n        url: \"https://example.com/coat?size=large&color=blue\",\n      },\n    },\n    // Reference to variants on other pages\n    { url: \"https://example.com/coat/medium-red\" },\n  ]}\n/>;\n```\n\n#### Multi-Page Variant Example\n\nUse this approach when each variant has its own page:\n\n```tsx\n// On a specific variant page\n<ProductJsonLd\n  name=\"Premium Leather Wallet - Brown Classic\"\n  sku=\"LW2024-BRN-CLS\"\n  color=\"Brown\"\n  pattern=\"Classic\"\n  material=\"Genuine Leather\"\n  isVariantOf={{ \"@id\": \"#wallet_group\" }}\n  inProductGroupWithID=\"LW2024\"\n  offers={{\n    price: 79.99,\n    priceCurrency: \"USD\",\n    availability: \"InStock\",\n  }}\n/>\n```\n\n#### ProductGroup Properties\n\n| Property         | Type                              | Description                                        |\n| ---------------- | --------------------------------- | -------------------------------------------------- |\n| `type`           | `\"ProductGroup\"`                  | Specifies ProductGroup type                        |\n| `productGroupID` | `string`                          | **Required.** Parent SKU or group identifier       |\n| `variesBy`       | `string \\| string[]`              | Properties that vary (size, color, material, etc.) |\n| `hasVariant`     | `Array<Product \\| {url: string}>` | Array of product variants                          |\n| `audience`       | `PeopleAudience`                  | Target audience (age, gender)                      |\n\n#### Variant Properties\n\nWhen defining variants in `hasVariant`, you can include:\n\n| Property   | Type           | Description                             |\n| ---------- | -------------- | --------------------------------------- |\n| `name`     | `string`       | Variant-specific name                   |\n| `sku`      | `string`       | Variant SKU                             |\n| `size`     | `string`       | Size value                              |\n| `color`    | `string`       | Color value                             |\n| `pattern`  | `string`       | Pattern type                            |\n| `material` | `string`       | Material composition                    |\n| `offers`   | `ProductOffer` | Variant-specific pricing                |\n| `url`      | `string`       | For referencing variants on other pages |\n\n#### Variant Reference Properties\n\nFor multi-page implementations, use these on individual product pages:\n\n| Property               | Type                            | Description                      |\n| ---------------------- | ------------------------------- | -------------------------------- |\n| `isVariantOf`          | `{@id: string} \\| ProductGroup` | Reference to parent ProductGroup |\n| `inProductGroupWithID` | `string`                        | Parent product group ID          |\n\n#### VariesBy Values\n\nSupported values for the `variesBy` property:\n\n- `\"size\"` or `\"https://schema.org/size\"`\n- `\"color\"` or `\"https://schema.org/color\"`\n- `\"material\"` or `\"https://schema.org/material\"`\n- `\"pattern\"` or `\"https://schema.org/pattern\"`\n- `\"suggestedAge\"` or `\"https://schema.org/suggestedAge\"`\n- `\"suggestedGender\"` or `\"https://schema.org/suggestedGender\"`\n\n#### Best Practices for Product Variants\n\n1. **Use ProductGroup** when you have multiple variants of the same product\n2. **Single-page approach**: Best when variants are selectable via dropdowns/buttons on one page\n3. **Multi-page approach**: Best when each variant needs its own SEO-optimized page\n4. **Always include productGroupID**: This links all variants together\n5. **Specify variesBy**: Clearly indicate which properties differentiate variants\n6. **Complete variant data**: Include as much variant-specific data as possible\n7. **URL references**: Use `{ url: \"...\" }` for variants on separate pages\n8. **Common properties**: Place shared properties (brand, material) at ProductGroup level\n9. **Unique identifiers**: Each variant should have unique SKU/GTIN\n10. **Consistent naming**: Use clear naming patterns for variants (e.g., \"Product - Size Color\")\n\n### ReviewJsonLd\n\nThe `ReviewJsonLd` component helps you add structured data for reviews to improve their appearance in search results. Reviews can appear as rich snippets with star ratings and review excerpts.\n\n#### Basic Usage\n\n```tsx\nimport { ReviewJsonLd } from \"next-seo\";\n\n<ReviewJsonLd\n  author=\"Bob Smith\"\n  reviewRating={{ ratingValue: 4 }}\n  itemReviewed=\"Legal Seafood\"\n/>;\n```\n\n#### Full Review Example\n\n```tsx\n<ReviewJsonLd\n  author={{ name: \"Sarah Johnson\", url: \"https://example.com/sarah\" }}\n  reviewRating={{\n    ratingValue: 5,\n    bestRating: 5,\n    worstRating: 1,\n  }}\n  itemReviewed={{\n    \"@type\": \"Restaurant\",\n    name: \"Legal Seafood\",\n    image: \"https://example.com/seafood.jpg\",\n    servesCuisine: \"Seafood\",\n    priceRange: \"$$$\",\n    telephone: \"1234567\",\n    address: {\n      streetAddress: \"123 William St\",\n      addressLocality: \"New York\",\n      addressRegion: \"NY\",\n      postalCode: \"10038\",\n      addressCountry: \"US\",\n    },\n  }}\n  reviewBody=\"Excellent seafood restaurant with fresh catches daily. The lobster was perfectly cooked and the service was outstanding.\"\n  datePublished=\"2024-01-15\"\n  publisher=\"FoodCritic Magazine\"\n  url=\"https://example.com/reviews/legal-seafood\"\n/>\n```\n\n#### Props\n\n| Property           | Type                               | Description                                  |\n| ------------------ | ---------------------------------- | -------------------------------------------- |\n| `author`           | `string \\| Person \\| Organization` | **Required.** The author of the review       |\n| `reviewRating`     | `Rating`                           | **Required.** The rating given in the review |\n| `itemReviewed`     | `string \\| ItemReviewed`           | **Required.** The item being reviewed        |\n| `datePublished`    | `string`                           | The date the review was published (ISO 8601) |\n| `reviewBody`       | `string`                           | The text of the review                       |\n| `publisher`        | `string \\| Person \\| Organization` | The publisher of the review                  |\n| `url`              | `string`                           | URL of the review                            |\n| `mainEntityOfPage` | `string \\| WebPage`                | Main entity of the page                      |\n\n#### Supported Item Types\n\nReviews can be written about various types of items:\n\n- `Book`\n- `Course`\n- `CreativeWorkSeason`\n- `CreativeWorkSeries`\n- `Episode`\n- `Event`\n- `Game`\n- `HowTo`\n- `LocalBusiness` (including restaurants)\n- `MediaObject`\n- `Movie`\n- `MusicPlaylist`\n- `MusicRecording`\n- `Organization`\n- `Product`\n- `Recipe`\n- `SoftwareApplication`\n\n#### Best Practices\n\n1. **Always include reviewBody**: Provides context for the rating\n2. **Use specific item types**: Specify `@type` for `itemReviewed` when possible\n3. **Include datePublished**: Helps establish review freshness\n4. **Author information**: Provide as much author detail as possible\n5. **Avoid self-serving reviews**: Don't mark up reviews on your own site about your organization\n\n### AggregateRatingJsonLd\n\nThe `AggregateRatingJsonLd` component helps you add structured data for aggregate ratings, showing the average rating from multiple reviews.\n\n#### Basic Usage\n\n```tsx\nimport { AggregateRatingJsonLd } from \"next-seo\";\n\n<AggregateRatingJsonLd\n  itemReviewed=\"Executive Anvil\"\n  ratingValue={4.4}\n  ratingCount={89}\n/>;\n```\n\n#### Full Example\n\n```tsx\n<AggregateRatingJsonLd\n  itemReviewed={{\n    \"@type\": \"Product\",\n    name: \"Executive Anvil\",\n    image: [\n      \"https://example.com/photos/1x1/photo.jpg\",\n      \"https://example.com/photos/4x3/photo.jpg\",\n      \"https://example.com/photos/16x9/photo.jpg\",\n    ],\n    brand: \"ACME\",\n  }}\n  ratingValue={88}\n  bestRating={100}\n  worstRating={0}\n  ratingCount={20}\n/>\n```\n\n#### With Review Count\n\n```tsx\n<AggregateRatingJsonLd\n  itemReviewed=\"Premium Coffee Maker\"\n  ratingValue={4.5}\n  reviewCount={127} // Use reviewCount instead of ratingCount\n  bestRating={5}\n/>\n```\n\n#### Props\n\n| Property       | Type                     | Description                                                                    |\n| -------------- | ------------------------ | ------------------------------------------------------------------------------ |\n| `itemReviewed` | `string \\| ItemReviewed` | **Required.** The item being rated                                             |\n| `ratingValue`  | `number \\| string`       | **Required.** The average rating value                                         |\n| `ratingCount`  | `number`                 | The total number of ratings                                                    |\n| `reviewCount`  | `number`                 | The number of reviews (at least one of ratingCount or reviewCount is required) |\n| `bestRating`   | `number`                 | The highest value in the rating system (default: 5)                            |\n| `worstRating`  | `number`                 | The lowest value in the rating system (default: 1)                             |\n\n#### Rating Scale\n\n- **Default scale**: 1 to 5 stars\n- **Custom scale**: Use `bestRating` and `worstRating` to define custom scales\n- **Percentages**: Use values like 88 with `bestRating: 100` for percentage-based ratings\n- **Fractions**: Supports decimal values like 4.4\n\n#### Best Practices\n\n1. **Choose the right count**: Use `ratingCount` for star ratings, `reviewCount` for written reviews\n2. **Specify scale for non-standard ratings**: Always include `bestRating` and `worstRating` for non-5-star scales\n3. **Combine with Product/Organization data**: Nest within or alongside main entity structured data\n4. **Minimum threshold**: Only use when you have multiple genuine ratings\n5. **Keep it updated**: Regularly update aggregate ratings as new reviews come in\n\n[↑ Back to Components](#-components-by-category)\n\n## Creating Custom Components\n\nNext SEO now supports creating your own custom JSON-LD components using the same utilities and patterns as the built-in components. This allows you to implement any Schema.org type while maintaining the excellent developer experience of next-seo.\n\n### Quick Example\n\n```tsx\nimport { JsonLdScript, processors } from \"next-seo\";\n\nexport function PodcastEpisodeJsonLd({ name, author, duration, url }) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"PodcastEpisode\",\n    name,\n    ...(url && { url }),\n    ...(duration && { duration }),\n    ...(author && { author: processors.processAuthor(author) }),\n  };\n\n  return <JsonLdScript data={data} scriptKey=\"podcast-episode\" />;\n}\n\n// Usage - no @type needed for author!\n<PodcastEpisodeJsonLd\n  name=\"Episode 1: Getting Started\"\n  author=\"Jane Doe\" // Simple string works!\n  duration=\"PT30M\"\n  url=\"https://example.com/episode-1\"\n/>;\n```\n\n### Key Features\n\n- **JsonLdScript Component**: Core component for rendering structured data\n- **60+ Processors**: Transform flexible inputs into Schema.org compliant objects\n- **@type Optional Pattern**: Users never need to specify `@type` manually\n- **TypeScript Support**: Full type safety with exported types\n\n### Available Utilities\n\nSee the [processors export file](./src/utils/processors.export.ts) for the complete list of available processors organized by category (People & Organizations, Media & Content, Locations & Places, Commerce & Offers, etc.).\n\n### Learn More\n\nFor comprehensive documentation on creating custom components, including:\n\n- Using built-in processors\n- Creating custom processors\n- Advanced patterns and best practices\n- Real-world examples\n\nSee the **[Custom Components Guide](./CUSTOM_COMPONENTS.md)**\n\n## Contributors\n\n[Contributing Guide](./CONTRIBUTING.md)\n\nA massive thank you to [everyone who contributes](https://github.com/garmeeh/next-seo/graphs/contributors) to this project 👏\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import js from \"@eslint/js\";\nimport globals from \"globals\";\nimport tseslint from \"typescript-eslint\";\nimport pluginReact from \"eslint-plugin-react\";\nimport { defineConfig } from \"eslint/config\";\n\nexport default defineConfig([\n  {\n    files: [\"**/*.{js,mjs,cjs,ts,jsx,tsx}\"],\n    plugins: { js },\n    extends: [\"js/recommended\"],\n  },\n  {\n    files: [\"**/*.{js,mjs,cjs,ts,jsx,tsx}\"],\n    languageOptions: { globals: globals.browser },\n  },\n  tseslint.configs.recommended,\n  pluginReact.configs.flat.recommended,\n  pluginReact.configs.flat[\"jsx-runtime\"],\n  {\n    settings: {\n      react: {\n        version: \"detect\",\n      },\n    },\n  },\n  {\n    ignores: [\"examples/\", \"dist/\", \"playwright-report/\", \"coverage/\"],\n  },\n]);\n"
  },
  {
    "path": "examples/app-router-showcase/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "examples/app-router-showcase/CLAUDE.md",
    "content": "# Example App Guidelines\n\n## Purpose\n\nThis Next.js app provides real example pages for E2E testing. Each page demonstrates a specific next-seo component with various configurations.\n\n## Commands\n\n```bash\npnpm example:dev    # Run on localhost:3001\npnpm example:build  # Build example app\npnpm example:start  # Start production build\n```\n\n## Critical Rules\n\n- **Every example page must be real and functional** - E2E tests depend on these\n- **Import components from \"next-seo\"** not from local paths\n- **Each component variation needs its own page** for E2E testing\n\n## Page Structure\n\n```\napp/\n├── [component]/              # Basic usage\n├── [component]-advanced/     # All features\n├── [component]-[variation]/  # Type variations\n└── [component]-special-chars/ # Special characters\n```\n\n## Naming Conventions\n\n- Basic: `/article`\n- Advanced: `/article-advanced`\n- Type variations: `/news-article`, `/blog-posting`\n- Special tests: `/article-special-chars`\n\n## Example Pattern\n\n```tsx\n// app/[component]/page.tsx\nimport { [Component]JsonLd } from \"next-seo\";\n\nexport default function [Component]Page() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <[Component]JsonLd\n        // Minimal required props for basic example\n        headline=\"Example Title\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Example Content</h1>\n        {/* Visual content for verification */}\n      </article>\n    </div>\n  );\n}\n```\n\n## Custom Components\n\nCustom JSON-LD components go in `components/custom/`:\n\n- Follow the same patterns as library components\n- Include example pages for testing\n- See `PodcastSeriesJsonLd.tsx` and `ServiceJsonLd.tsx` as references\n\n## Key Requirements\n\n1. **Real content only** - No mocked data\n2. **Consistent data** - Use predictable values for E2E assertions\n3. **Cover all props** - Advanced examples should demonstrate all features\n4. **Valid HTML** - Include semantic markup for accessibility\n\n## Notes\n\n- App runs on port 3001 to avoid conflicts\n- Used exclusively for E2E testing\n- Keep examples focused and minimal\n- Data should be predictable for test assertions\n"
  },
  {
    "path": "examples/app-router-showcase/README.md",
    "content": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n# or\npnpm dev\n# or\nbun dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.\n\nThis project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.\n"
  },
  {
    "path": "examples/app-router-showcase/app/aggregate-rating/page.tsx",
    "content": "import { AggregateRatingJsonLd } from \"next-seo\";\n\nexport default function Page() {\n  return (\n    <div className=\"container mx-auto p-8 space-y-6\">\n      <h1 className=\"text-2xl font-semibold\">AggregateRatingJsonLd Example</h1>\n      <AggregateRatingJsonLd\n        itemReviewed={{ \"@type\": \"Product\", name: \"Executive Anvil\" }}\n        ratingValue={4.4}\n        ratingCount={89}\n      />\n      <p>\n        This page demonstrates a standalone AggregateRating JSON-LD for a\n        product.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/aggregate-rating-restaurant/page.tsx",
    "content": "import { AggregateRatingJsonLd } from \"next-seo\";\n\nexport default function Page() {\n  return (\n    <div className=\"container mx-auto p-8 space-y-6\">\n      <h1 className=\"text-2xl font-semibold\">Restaurant Aggregate Rating</h1>\n      <AggregateRatingJsonLd\n        itemReviewed={{\n          \"@type\": \"LocalBusiness\",\n          name: \"Legal Seafood\",\n          image: \"https://example.com/seafood-restaurant.jpg\",\n          servesCuisine: \"Seafood\",\n          telephone: \"1234567\",\n          priceRange: \"$$$\",\n          address: {\n            \"@type\": \"PostalAddress\",\n            streetAddress: \"123 William St\",\n            addressLocality: \"New York\",\n            addressRegion: \"NY\",\n            postalCode: \"10038\",\n            addressCountry: \"US\",\n          },\n        }}\n        ratingValue={88}\n        bestRating={100}\n        ratingCount={350}\n      />\n      <div className=\"prose max-w-none\">\n        <h2>Restaurant Rating Summary</h2>\n        <p>\n          This example demonstrates an aggregate rating for a restaurant using a\n          percentage-based rating system (0-100).\n        </p>\n        <h3>Rating Details:</h3>\n        <ul>\n          <li>Overall Score: 88/100</li>\n          <li>Based on 350 ratings</li>\n          <li>Restaurant includes full location and contact details</li>\n        </ul>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/article/page.tsx",
    "content": "import { ArticleJsonLd } from \"next-seo\";\n\nexport default function ArticlePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ArticleJsonLd\n        headline=\"Understanding Next.js App Router\"\n        url=\"https://example.com/articles/nextjs-app-router\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        author=\"Sarah Johnson\"\n        image=\"https://example.com/images/nextjs-article.jpg\"\n        description=\"A comprehensive guide to Next.js App Router and its features\"\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Understanding Next.js App Router</h1>\n        <p className=\"text-gray-600\">By Sarah Johnson | January 1, 2024</p>\n\n        <p>\n          The Next.js App Router represents a significant evolution in how we\n          build React applications. This article explores its key features and\n          benefits.\n        </p>\n\n        <h2>Server Components</h2>\n        <p>\n          React Server Components allow us to render components on the server,\n          reducing the JavaScript bundle size sent to the client...\n        </p>\n\n        <h2>Nested Layouts</h2>\n        <p>\n          The App Router introduces a powerful nested layout system that makes\n          it easy to share UI between routes...\n        </p>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/blog-posting/page.tsx",
    "content": "import { ArticleJsonLd } from \"next-seo\";\n\nexport default function BlogPostingPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ArticleJsonLd\n        type=\"BlogPosting\"\n        headline=\"10 Essential SEO Tips for Modern Web Development\"\n        url=\"https://example.com/blog/seo-tips-web-development\"\n        datePublished=\"2024-01-10T09:00:00+00:00\"\n        author={{\n          name: \"WebDev Solutions\",\n          url: \"https://example.com\",\n          logo: \"https://example.com/webdev-logo.png\",\n        }}\n        image={{\n          \"@type\": \"ImageObject\",\n          url: \"https://example.com/images/seo-tips-hero.jpg\",\n          width: 1920,\n          height: 1080,\n          caption: \"SEO Tips for Web Developers\",\n        }}\n        publisher={{\n          \"@type\": \"Organization\",\n          name: \"WebDev Solutions Blog\",\n          logo: {\n            \"@type\": \"ImageObject\",\n            url: \"https://example.com/blog-logo.png\",\n            width: 200,\n            height: 60,\n          },\n        }}\n        description=\"Learn the essential SEO strategies every web developer should know to improve search rankings\"\n        isAccessibleForFree={false}\n        mainEntityOfPage={{\n          \"@type\": \"WebPage\",\n          \"@id\": \"https://example.com/blog/seo-tips-web-development\",\n        }}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <div className=\"bg-blue-600 text-white px-4 py-2 rounded-lg inline-block mb-4\">\n          PREMIUM CONTENT\n        </div>\n\n        <h1>10 Essential SEO Tips for Modern Web Development</h1>\n\n        <div className=\"text-gray-600 mb-8\">\n          <p>By WebDev Solutions Team</p>\n          <p>Published: January 10, 2024</p>\n        </div>\n\n        <div className=\"bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-8\">\n          <p className=\"font-semibold\">Premium Article</p>\n          <p>This is premium content. Subscribe to access the full article.</p>\n        </div>\n\n        <p className=\"lead text-xl\">\n          Search Engine Optimization (SEO) is crucial for modern web\n          development. In this comprehensive guide, we'll explore the top 10\n          strategies that every developer should implement.\n        </p>\n\n        <h2>1. Optimize Page Load Speed</h2>\n        <p>\n          Page speed is a critical ranking factor. Use tools like Lighthouse to\n          measure and improve your Core Web Vitals...\n        </p>\n\n        <h2>2. Implement Structured Data</h2>\n        <p>\n          Using structured data helps search engines understand your content\n          better. Tools like Next SEO make this implementation\n          straightforward...\n        </p>\n\n        <h2>3. Mobile-First Design</h2>\n        <p>\n          With mobile-first indexing, ensuring your site works perfectly on\n          mobile devices is no longer optional...\n        </p>\n\n        <div className=\"bg-gray-100 p-6 rounded-lg mt-8\">\n          <h3>About the Author</h3>\n          <p>\n            WebDev Solutions is a leading web development agency specializing in\n            modern, SEO-optimized web applications.\n          </p>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/breadcrumb/advanced/page.tsx",
    "content": "import { BreadcrumbJsonLd } from \"next-seo\";\n\nexport default function AdvancedBreadcrumbPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <BreadcrumbJsonLd\n        items={[\n          {\n            name: \"Home\",\n            item: \"https://example.com\",\n          },\n          {\n            name: \"Blog\",\n            item: { \"@id\": \"https://example.com/blog\" },\n          },\n          {\n            name: \"Technology\",\n            item: { \"@id\": \"https://example.com/blog/technology\" },\n          },\n          {\n            name: \"Understanding JSON-LD and Structured Data\",\n          },\n        ]}\n        scriptId=\"blog-breadcrumb\"\n        scriptKey=\"blog-breadcrumb-key\"\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <h1>Advanced Breadcrumb Example</h1>\n\n        <nav aria-label=\"Breadcrumb\" className=\"my-4\">\n          <ol className=\"flex items-center space-x-2\">\n            <li>Home</li>\n            <li className=\"text-gray-500\">›</li>\n            <li>\n              <a href=\"/blog\" className=\"text-blue-600 hover:underline\">\n                Blog\n              </a>\n            </li>\n            <li className=\"text-gray-500\">›</li>\n            <li>\n              <a\n                href=\"/blog/technology\"\n                className=\"text-blue-600 hover:underline\"\n              >\n                Technology\n              </a>\n            </li>\n            <li className=\"text-gray-500\">›</li>\n            <li className=\"text-gray-700\">\n              Understanding JSON-LD and Structured Data\n            </li>\n          </ol>\n        </nav>\n\n        <article>\n          <h2>Understanding JSON-LD and Structured Data</h2>\n          <p className=\"text-gray-600 mb-4\">Published on November 15, 2024</p>\n\n          <p>\n            This example demonstrates advanced features of the BreadcrumbJsonLd\n            component, including the use of Thing objects with @id properties\n            and custom script attributes.\n          </p>\n\n          <h3>Features Demonstrated</h3>\n          <ul>\n            <li>Using Thing objects with @id instead of plain URL strings</li>\n            <li>Custom scriptId for targeting specific scripts</li>\n            <li>Custom scriptKey for testing purposes</li>\n            <li>Mixed usage of URL strings and Thing objects</li>\n          </ul>\n\n          <h3>When to Use Thing Objects</h3>\n          <p>\n            While plain URL strings work perfectly fine for most use cases,\n            using Thing objects with @id can be beneficial when:\n          </p>\n          <ul>\n            <li>\n              You need to maintain consistency with other structured data on\n              your page\n            </li>\n            <li>You're integrating with systems that expect Thing objects</li>\n            <li>\n              You want to be explicit about the semantic meaning of the\n              reference\n            </li>\n          </ul>\n\n          <h3>Custom Script Attributes</h3>\n          <p>\n            The <code>scriptId</code> and <code>scriptKey</code> props allow you\n            to:\n          </p>\n          <ul>\n            <li>Target specific JSON-LD scripts with JavaScript if needed</li>\n            <li>\n              Differentiate between multiple structured data blocks in tests\n            </li>\n            <li>Maintain consistent identifiers across page renders</li>\n          </ul>\n        </article>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/breadcrumb/multiple/page.tsx",
    "content": "import { BreadcrumbJsonLd } from \"next-seo\";\n\nexport default function MultipleBreadcrumbsPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <BreadcrumbJsonLd\n        multipleTrails={[\n          // First trail: Category path\n          [\n            {\n              name: \"Books\",\n              item: \"https://example.com/books\",\n            },\n            {\n              name: \"Science Fiction\",\n              item: \"https://example.com/books/sciencefiction\",\n            },\n            {\n              name: \"Award Winners\",\n            },\n          ],\n          // Second trail: Award path\n          [\n            {\n              name: \"Literature\",\n              item: \"https://example.com/literature\",\n            },\n            {\n              name: \"Award Winners\",\n            },\n          ],\n        ]}\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <h1>Multiple Breadcrumb Trails Example</h1>\n\n        <p>\n          This page can be reached through multiple paths, so we provide\n          multiple breadcrumb trails for search engines.\n        </p>\n\n        <h2>Path 1: By Category</h2>\n        <nav aria-label=\"Breadcrumb by category\" className=\"my-4\">\n          <ol className=\"flex items-center space-x-2\">\n            <li>Books</li>\n            <li className=\"text-gray-500\">›</li>\n            <li>\n              <a\n                href=\"/books/sciencefiction\"\n                className=\"text-blue-600 hover:underline\"\n              >\n                Science Fiction\n              </a>\n            </li>\n            <li className=\"text-gray-500\">›</li>\n            <li className=\"text-gray-700\">Award Winners</li>\n          </ol>\n        </nav>\n\n        <h2>Path 2: By Award Type</h2>\n        <nav aria-label=\"Breadcrumb by award\" className=\"my-4\">\n          <ol className=\"flex items-center space-x-2\">\n            <li>\n              <a href=\"/literature\" className=\"text-blue-600 hover:underline\">\n                Literature\n              </a>\n            </li>\n            <li className=\"text-gray-500\">›</li>\n            <li className=\"text-gray-700\">Award Winners</li>\n          </ol>\n        </nav>\n\n        <h2>Why Multiple Breadcrumbs?</h2>\n        <p>\n          Some pages can logically belong to multiple hierarchies. For example,\n          this \"Award Winners\" page can be reached both through the Books →\n          Science Fiction path and through the Literature path. By providing\n          multiple breadcrumb trails, we help search engines understand all the\n          ways users might navigate to this content.\n        </p>\n\n        <h2>Implementation Note</h2>\n        <p>\n          When using multiple trails, the component generates an array of\n          BreadcrumbList objects instead of a single object. This is the correct\n          way to represent multiple breadcrumb trails according to Google's\n          structured data guidelines.\n        </p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/breadcrumb/page.tsx",
    "content": "import { BreadcrumbJsonLd } from \"next-seo\";\n\nexport default function BreadcrumbPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <BreadcrumbJsonLd\n        items={[\n          {\n            name: \"Home\",\n            item: \"https://example.com\",\n          },\n          {\n            name: \"Products\",\n            item: \"https://example.com/products\",\n          },\n          {\n            name: \"Electronics\",\n            item: \"https://example.com/products/electronics\",\n          },\n          {\n            name: \"Smartphones\",\n            item: \"https://example.com/products/electronics/smartphones\",\n          },\n          {\n            name: \"iPhone 15 Pro\",\n          },\n        ]}\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <h1>Breadcrumb JSON-LD Example</h1>\n\n        <nav aria-label=\"Breadcrumb\" className=\"my-4\">\n          <ol className=\"flex items-center space-x-2\">\n            <li>Home</li>\n            <li className=\"text-gray-500\">/</li>\n            <li>\n              <a href=\"/products\" className=\"text-blue-600 hover:underline\">\n                Products\n              </a>\n            </li>\n            <li className=\"text-gray-500\">/</li>\n            <li>\n              <a\n                href=\"/products/electronics\"\n                className=\"text-blue-600 hover:underline\"\n              >\n                Electronics\n              </a>\n            </li>\n            <li className=\"text-gray-500\">/</li>\n            <li>\n              <a\n                href=\"/products/electronics/smartphones\"\n                className=\"text-blue-600 hover:underline\"\n              >\n                Smartphones\n              </a>\n            </li>\n            <li className=\"text-gray-500\">/</li>\n            <li className=\"text-gray-700\">iPhone 15 Pro</li>\n          </ol>\n        </nav>\n\n        <p>\n          This page demonstrates the basic usage of the BreadcrumbJsonLd\n          component. The breadcrumb trail shows the hierarchical path to reach\n          this product page.\n        </p>\n\n        <h2>Features Demonstrated</h2>\n        <ul>\n          <li>Simple breadcrumb trail with multiple levels</li>\n          <li>Last item without URL (current page)</li>\n          <li>Automatic position numbering</li>\n        </ul>\n\n        <h2>View Source</h2>\n        <p>\n          Inspect the page source to see the generated JSON-LD structured data\n          in the head section.\n        </p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/carousel-course/page.tsx",
    "content": "import { CarouselJsonLd } from \"next-seo\";\n\nexport default function CarouselCoursePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <CarouselJsonLd\n        contentType=\"Course\"\n        items={[\n          {\n            name: \"Introduction to Web Development\",\n            description:\n              \"Learn the fundamentals of HTML, CSS, and JavaScript to build modern websites\",\n            url: \"https://example.com/courses/web-development-intro\",\n            provider: \"Tech Academy Online\",\n          },\n          {\n            name: \"React.js Masterclass\",\n            description:\n              \"Master React.js from basics to advanced concepts including hooks, context, and performance optimization\",\n            url: \"https://example.com/courses/react-masterclass\",\n            provider: {\n              name: \"Code School Pro\",\n              url: \"https://example.com/school\",\n            },\n          },\n          {\n            name: \"Advanced TypeScript\",\n            description:\n              \"Deep dive into TypeScript's type system, generics, and advanced patterns\",\n            provider: {\n              name: \"Developer Institute\",\n              sameAs: [\"https://twitter.com/devinstitute\"],\n            },\n          },\n          {\n            name: \"Full-Stack Development with Next.js\",\n            description:\n              \"Build production-ready full-stack applications with Next.js, TypeScript, and Tailwind CSS\",\n            url: \"https://example.com/courses/nextjs-fullstack\",\n            provider: \"Modern Web Academy\",\n          },\n        ]}\n      />\n\n      <h1 className=\"text-4xl font-bold mb-8\">Course Carousel Example</h1>\n\n      <div className=\"prose lg:prose-xl\">\n        <p>\n          Explore our top-rated web development courses designed to take your\n          skills to the next level.\n        </p>\n\n        <h2>Featured Courses</h2>\n\n        <div className=\"grid gap-6 mt-6\">\n          <div className=\"border p-6 rounded-lg shadow\">\n            <h3 className=\"text-2xl font-semibold\">\n              Introduction to Web Development\n            </h3>\n            <p className=\"text-gray-600 mt-2\">\n              Learn the fundamentals of HTML, CSS, and JavaScript to build\n              modern websites\n            </p>\n            <p className=\"text-sm text-gray-500 mt-2\">\n              Provider: Tech Academy Online\n            </p>\n            <a\n              href=\"https://example.com/courses/web-development-intro\"\n              className=\"text-blue-600 mt-4 inline-block\"\n            >\n              Enroll Now →\n            </a>\n          </div>\n\n          <div className=\"border p-6 rounded-lg shadow\">\n            <h3 className=\"text-2xl font-semibold\">React.js Masterclass</h3>\n            <p className=\"text-gray-600 mt-2\">\n              Master React.js from basics to advanced concepts including hooks,\n              context, and performance optimization\n            </p>\n            <p className=\"text-sm text-gray-500 mt-2\">\n              Provider: Code School Pro\n            </p>\n            <a\n              href=\"https://example.com/courses/react-masterclass\"\n              className=\"text-blue-600 mt-4 inline-block\"\n            >\n              Enroll Now →\n            </a>\n          </div>\n\n          <div className=\"border p-6 rounded-lg shadow\">\n            <h3 className=\"text-2xl font-semibold\">Advanced TypeScript</h3>\n            <p className=\"text-gray-600 mt-2\">\n              Deep dive into TypeScript's type system, generics, and advanced\n              patterns\n            </p>\n            <p className=\"text-sm text-gray-500 mt-2\">\n              Provider: Developer Institute\n            </p>\n            <button className=\"bg-blue-600 text-white px-4 py-2 rounded mt-4\">\n              Coming Soon\n            </button>\n          </div>\n\n          <div className=\"border p-6 rounded-lg shadow\">\n            <h3 className=\"text-2xl font-semibold\">\n              Full-Stack Development with Next.js\n            </h3>\n            <p className=\"text-gray-600 mt-2\">\n              Build production-ready full-stack applications with Next.js,\n              TypeScript, and Tailwind CSS\n            </p>\n            <p className=\"text-sm text-gray-500 mt-2\">\n              Provider: Modern Web Academy\n            </p>\n            <a\n              href=\"https://example.com/courses/nextjs-fullstack\"\n              className=\"text-blue-600 mt-4 inline-block\"\n            >\n              Enroll Now →\n            </a>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/carousel-movie/page.tsx",
    "content": "import { CarouselJsonLd } from \"next-seo\";\n\nexport default function CarouselMoviePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <CarouselJsonLd\n        contentType=\"Movie\"\n        items={[\n          {\n            name: \"The Matrix\",\n            image: [\n              \"https://example.com/matrix-poster.jpg\",\n              \"https://example.com/matrix-wide.jpg\",\n            ],\n            url: \"https://example.com/movies/the-matrix\",\n            dateCreated: \"1999-03-31\",\n            director: \"The Wachowskis\",\n            aggregateRating: {\n              ratingValue: 8.7,\n              bestRating: 10,\n              ratingCount: 1897563,\n            },\n            review: {\n              reviewRating: { ratingValue: 9 },\n              author: \"Film Critic Daily\",\n              reviewBody:\n                \"A groundbreaking sci-fi masterpiece that redefined action cinema\",\n            },\n          },\n          {\n            name: \"Inception\",\n            image: \"https://example.com/inception-poster.jpg\",\n            url: \"https://example.com/movies/inception\",\n            dateCreated: \"2010-07-16\",\n            director: {\n              name: \"Christopher Nolan\",\n              url: \"https://example.com/directors/nolan\",\n            },\n            aggregateRating: {\n              ratingValue: 8.8,\n              bestRating: 10,\n              ratingCount: 2432198,\n            },\n          },\n          {\n            name: \"Interstellar\",\n            image: [\n              \"https://example.com/interstellar-poster.jpg\",\n              \"https://example.com/interstellar-banner.jpg\",\n              \"https://example.com/interstellar-square.jpg\",\n            ],\n            dateCreated: \"2014-11-07\",\n            director: \"Christopher Nolan\",\n            review: {\n              reviewRating: {\n                ratingValue: 5,\n                bestRating: 5,\n              },\n              author: {\n                name: \"Space & Science Review\",\n              },\n              reviewBody:\n                \"A visually stunning and emotionally powerful journey through space and time\",\n            },\n            aggregateRating: {\n              ratingValue: 8.6,\n              ratingCount: 1892456,\n            },\n          },\n          {\n            name: \"Blade Runner 2049\",\n            image: \"https://example.com/blade-runner-2049.jpg\",\n            url: \"https://example.com/movies/blade-runner-2049\",\n            dateCreated: \"2017-10-06\",\n            director: \"Denis Villeneuve\",\n            aggregateRating: {\n              ratingValue: 8.0,\n              ratingCount: 673421,\n            },\n          },\n        ]}\n      />\n\n      <h1 className=\"text-4xl font-bold mb-8\">Movie Carousel Example</h1>\n\n      <div className=\"prose lg:prose-xl\">\n        <p>\n          Discover the best sci-fi movies that have defined the genre and\n          captivated audiences worldwide.\n        </p>\n\n        <h2>Top Sci-Fi Movies</h2>\n\n        <div className=\"grid gap-6 mt-6\">\n          <div className=\"border p-6 rounded-lg shadow flex gap-6\">\n            <div className=\"w-32 h-48 bg-gray-200 rounded flex-shrink-0\"></div>\n            <div className=\"flex-1\">\n              <h3 className=\"text-2xl font-semibold\">The Matrix</h3>\n              <p className=\"text-gray-600 mt-2\">\n                A computer hacker learns about the true nature of reality and\n                his role in the war against its controllers.\n              </p>\n              <p className=\"text-sm text-gray-500 mt-2\">\n                Director: The Wachowskis | Released: 1999\n              </p>\n              <div className=\"flex items-center gap-4 mt-3\">\n                <span className=\"text-yellow-500\">★ 8.7/10</span>\n                <span className=\"text-gray-500 text-sm\">(1.9M ratings)</span>\n              </div>\n              <a\n                href=\"https://example.com/movies/the-matrix\"\n                className=\"text-blue-600 mt-4 inline-block\"\n              >\n                Watch Now →\n              </a>\n            </div>\n          </div>\n\n          <div className=\"border p-6 rounded-lg shadow flex gap-6\">\n            <div className=\"w-32 h-48 bg-gray-200 rounded flex-shrink-0\"></div>\n            <div className=\"flex-1\">\n              <h3 className=\"text-2xl font-semibold\">Inception</h3>\n              <p className=\"text-gray-600 mt-2\">\n                A thief who steals corporate secrets through dream-sharing\n                technology is given the inverse task of planting an idea.\n              </p>\n              <p className=\"text-sm text-gray-500 mt-2\">\n                Director: Christopher Nolan | Released: 2010\n              </p>\n              <div className=\"flex items-center gap-4 mt-3\">\n                <span className=\"text-yellow-500\">★ 8.8/10</span>\n                <span className=\"text-gray-500 text-sm\">(2.4M ratings)</span>\n              </div>\n              <a\n                href=\"https://example.com/movies/inception\"\n                className=\"text-blue-600 mt-4 inline-block\"\n              >\n                Watch Now →\n              </a>\n            </div>\n          </div>\n\n          <div className=\"border p-6 rounded-lg shadow flex gap-6\">\n            <div className=\"w-32 h-48 bg-gray-200 rounded flex-shrink-0\"></div>\n            <div className=\"flex-1\">\n              <h3 className=\"text-2xl font-semibold\">Interstellar</h3>\n              <p className=\"text-gray-600 mt-2\">\n                A team of explorers travel through a wormhole in space in an\n                attempt to ensure humanity's survival.\n              </p>\n              <p className=\"text-sm text-gray-500 mt-2\">\n                Director: Christopher Nolan | Released: 2014\n              </p>\n              <div className=\"flex items-center gap-4 mt-3\">\n                <span className=\"text-yellow-500\">★ 8.6/10</span>\n                <span className=\"text-gray-500 text-sm\">(1.9M ratings)</span>\n              </div>\n              <button className=\"bg-blue-600 text-white px-4 py-2 rounded mt-4\">\n                Watch Trailer\n              </button>\n            </div>\n          </div>\n\n          <div className=\"border p-6 rounded-lg shadow flex gap-6\">\n            <div className=\"w-32 h-48 bg-gray-200 rounded flex-shrink-0\"></div>\n            <div className=\"flex-1\">\n              <h3 className=\"text-2xl font-semibold\">Blade Runner 2049</h3>\n              <p className=\"text-gray-600 mt-2\">\n                A young blade runner's discovery of a long-buried secret leads\n                him to track down former blade runner Rick Deckard.\n              </p>\n              <p className=\"text-sm text-gray-500 mt-2\">\n                Director: Denis Villeneuve | Released: 2017\n              </p>\n              <div className=\"flex items-center gap-4 mt-3\">\n                <span className=\"text-yellow-500\">★ 8.0/10</span>\n                <span className=\"text-gray-500 text-sm\">(673K ratings)</span>\n              </div>\n              <a\n                href=\"https://example.com/movies/blade-runner-2049\"\n                className=\"text-blue-600 mt-4 inline-block\"\n              >\n                Watch Now →\n              </a>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/carousel-recipe/page.tsx",
    "content": "import { CarouselJsonLd } from \"next-seo\";\n\nexport default function CarouselRecipePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <CarouselJsonLd\n        contentType=\"Recipe\"\n        items={[\n          {\n            name: \"Perfect Chocolate Chip Cookies\",\n            image: [\n              \"https://example.com/cookies-1x1.jpg\",\n              \"https://example.com/cookies-4x3.jpg\",\n              \"https://example.com/cookies-16x9.jpg\",\n            ],\n            description:\n              \"Crispy on the outside, chewy on the inside chocolate chip cookies\",\n            author: \"Chef Sarah\",\n            datePublished: \"2024-01-15\",\n            prepTime: \"PT20M\",\n            cookTime: \"PT12M\",\n            totalTime: \"PT32M\",\n            recipeYield: \"24 cookies\",\n            recipeCategory: \"Dessert\",\n            recipeCuisine: \"American\",\n            recipeIngredient: [\n              \"2 1/4 cups all-purpose flour\",\n              \"1 cup butter, softened\",\n              \"3/4 cup granulated sugar\",\n              \"3/4 cup packed brown sugar\",\n              \"2 large eggs\",\n              \"1 teaspoon vanilla extract\",\n              \"1 teaspoon baking soda\",\n              \"1 teaspoon salt\",\n              \"2 cups chocolate chips\",\n            ],\n            recipeInstructions: [\n              \"Preheat oven to 375°F (190°C)\",\n              \"Mix flour, baking soda, and salt in a bowl\",\n              \"Beat butter and sugars until creamy\",\n              \"Add eggs and vanilla to butter mixture\",\n              \"Gradually blend in flour mixture\",\n              \"Stir in chocolate chips\",\n              \"Drop by rounded tablespoon onto ungreased cookie sheets\",\n              \"Bake 9-11 minutes or until golden brown\",\n            ],\n            nutrition: {\n              calories: \"210 calories\",\n              proteinContent: \"2g\",\n              carbohydrateContent: \"28g\",\n              fatContent: \"10g\",\n              saturatedFatContent: \"6g\",\n              sugarContent: \"18g\",\n              servingSize: \"1 cookie\",\n            },\n            aggregateRating: {\n              ratingValue: 4.8,\n              ratingCount: 3421,\n            },\n            keywords: \"chocolate chip cookies, dessert, baking\",\n            url: \"https://example.com/recipes/chocolate-chip-cookies\",\n          },\n          {\n            name: \"Classic Banana Bread\",\n            image: \"https://example.com/banana-bread.jpg\",\n            description:\n              \"Moist and flavorful banana bread with a perfect texture\",\n            author: \"Grandma Rose\",\n            prepTime: \"PT15M\",\n            cookTime: \"PT60M\",\n            totalTime: \"PT75M\",\n            recipeYield: 1,\n            recipeCategory: \"Bread\",\n            recipeCuisine: \"American\",\n            recipeIngredient: [\n              \"3 ripe bananas, mashed\",\n              \"1 3/4 cups all-purpose flour\",\n              \"3/4 cup sugar\",\n              \"1 egg, beaten\",\n              \"1/3 cup melted butter\",\n              \"1 teaspoon baking soda\",\n              \"1 teaspoon salt\",\n              \"1 teaspoon vanilla extract\",\n            ],\n            recipeInstructions: {\n              name: \"Baking Process\",\n              itemListElement: [\n                { text: \"Preheat oven to 350°F (175°C)\" },\n                { text: \"Mix dry ingredients\" },\n                { text: \"Combine wet ingredients\" },\n                { text: \"Fold together gently\" },\n                { text: \"Pour into greased loaf pan\" },\n                { text: \"Bake for 60 minutes\" },\n              ],\n            },\n            aggregateRating: {\n              ratingValue: 4.9,\n              ratingCount: 892,\n            },\n            video: {\n              name: \"How to Make Banana Bread\",\n              description: \"Step-by-step banana bread tutorial\",\n              thumbnailUrl: \"https://example.com/banana-bread-thumb.jpg\",\n              contentUrl: \"https://example.com/videos/banana-bread.mp4\",\n              uploadDate: \"2024-01-10\",\n              duration: \"PT8M\",\n            },\n          },\n          {\n            name: \"Homemade Pizza Margherita\",\n            image: {\n              url: \"https://example.com/pizza-margherita.jpg\",\n              width: 1200,\n              height: 800,\n            },\n            description:\n              \"Authentic Italian pizza with fresh mozzarella, tomatoes, and basil\",\n            author: \"Chef Giovanni\",\n            prepTime: \"PT30M\",\n            cookTime: \"PT15M\",\n            totalTime: \"PT45M\",\n            recipeYield: 2,\n            recipeCategory: \"Main Course\",\n            recipeCuisine: \"Italian\",\n            recipeIngredient: [\n              \"Pizza dough for 2 pizzas\",\n              \"1 cup tomato sauce\",\n              \"16 oz fresh mozzarella\",\n              \"Fresh basil leaves\",\n              \"Extra virgin olive oil\",\n              \"Salt and pepper to taste\",\n            ],\n            recipeInstructions: [\n              { text: \"Roll out pizza dough\", name: \"Prepare dough\" },\n              { text: \"Spread tomato sauce evenly\" },\n              { text: \"Add torn mozzarella pieces\" },\n              { text: \"Drizzle with olive oil\" },\n              { text: \"Bake at 500°F for 12-15 minutes\" },\n              { text: \"Top with fresh basil before serving\" },\n            ],\n            nutrition: {\n              calories: \"320 calories\",\n              proteinContent: \"14g\",\n              carbohydrateContent: \"42g\",\n              fatContent: \"12g\",\n            },\n            aggregateRating: {\n              ratingValue: 4.7,\n              ratingCount: 567,\n            },\n            url: \"https://example.com/recipes/pizza-margherita\",\n          },\n        ]}\n      />\n\n      <h1 className=\"text-4xl font-bold mb-8\">Recipe Carousel Example</h1>\n\n      <div className=\"prose lg:prose-xl\">\n        <p>\n          Explore our collection of delicious recipes that are perfect for any\n          occasion.\n        </p>\n\n        <h2>Featured Recipes</h2>\n\n        <div className=\"grid gap-6 mt-6\">\n          <div className=\"border p-6 rounded-lg shadow\">\n            <div className=\"h-48 bg-gray-200 rounded mb-4\"></div>\n            <h3 className=\"text-2xl font-semibold\">\n              Perfect Chocolate Chip Cookies\n            </h3>\n            <p className=\"text-gray-600 mt-2\">\n              Crispy on the outside, chewy on the inside chocolate chip cookies\n            </p>\n            <div className=\"flex items-center gap-4 mt-3 text-sm text-gray-600\">\n              <span>⏱ 32 min</span>\n              <span>🍽 24 cookies</span>\n              <span className=\"text-yellow-500\">★ 4.8 (3,421)</span>\n            </div>\n            <div className=\"mt-4\">\n              <h4 className=\"font-semibold\">Ingredients:</h4>\n              <ul className=\"text-sm text-gray-600 mt-2\">\n                <li>• 2 1/4 cups flour</li>\n                <li>• 1 cup butter</li>\n                <li>• 2 cups chocolate chips</li>\n                <li>• And more...</li>\n              </ul>\n            </div>\n            <a\n              href=\"https://example.com/recipes/chocolate-chip-cookies\"\n              className=\"text-blue-600 mt-4 inline-block\"\n            >\n              View Full Recipe →\n            </a>\n          </div>\n\n          <div className=\"border p-6 rounded-lg shadow\">\n            <div className=\"h-48 bg-gray-200 rounded mb-4\"></div>\n            <h3 className=\"text-2xl font-semibold\">Classic Banana Bread</h3>\n            <p className=\"text-gray-600 mt-2\">\n              Moist and flavorful banana bread with a perfect texture\n            </p>\n            <div className=\"flex items-center gap-4 mt-3 text-sm text-gray-600\">\n              <span>⏱ 75 min</span>\n              <span>🍽 1 loaf</span>\n              <span className=\"text-yellow-500\">★ 4.9 (892)</span>\n            </div>\n            <div className=\"mt-4\">\n              <h4 className=\"font-semibold\">Key Ingredients:</h4>\n              <ul className=\"text-sm text-gray-600 mt-2\">\n                <li>• 3 ripe bananas</li>\n                <li>• 1 3/4 cups flour</li>\n                <li>• 1/3 cup melted butter</li>\n                <li>• And more...</li>\n              </ul>\n            </div>\n            <div className=\"mt-3\">\n              <span className=\"text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded\">\n                📹 Video Available\n              </span>\n            </div>\n            <button className=\"bg-blue-600 text-white px-4 py-2 rounded mt-4\">\n              Watch Video Tutorial\n            </button>\n          </div>\n\n          <div className=\"border p-6 rounded-lg shadow\">\n            <div className=\"h-48 bg-gray-200 rounded mb-4\"></div>\n            <h3 className=\"text-2xl font-semibold\">\n              Homemade Pizza Margherita\n            </h3>\n            <p className=\"text-gray-600 mt-2\">\n              Authentic Italian pizza with fresh mozzarella, tomatoes, and basil\n            </p>\n            <div className=\"flex items-center gap-4 mt-3 text-sm text-gray-600\">\n              <span>⏱ 45 min</span>\n              <span>🍽 2 pizzas</span>\n              <span className=\"text-yellow-500\">★ 4.7 (567)</span>\n            </div>\n            <div className=\"mt-4\">\n              <h4 className=\"font-semibold\">Main Ingredients:</h4>\n              <ul className=\"text-sm text-gray-600 mt-2\">\n                <li>• Pizza dough</li>\n                <li>• Fresh mozzarella</li>\n                <li>• Tomato sauce</li>\n                <li>• Fresh basil</li>\n              </ul>\n            </div>\n            <a\n              href=\"https://example.com/recipes/pizza-margherita\"\n              className=\"text-blue-600 mt-4 inline-block\"\n            >\n              View Full Recipe →\n            </a>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/carousel-restaurant/page.tsx",
    "content": "import { CarouselJsonLd } from \"next-seo\";\n\nexport default function CarouselRestaurantPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <CarouselJsonLd\n        contentType=\"Restaurant\"\n        items={[\n          {\n            name: \"Luigi's Italian Bistro\",\n            address: \"123 Main Street, New York, NY 10001\",\n            telephone: \"+1-212-555-0100\",\n            url: \"https://example.com/restaurants/luigis\",\n            image: [\n              \"https://example.com/luigis-exterior.jpg\",\n              \"https://example.com/luigis-interior.jpg\",\n            ],\n            priceRange: \"$$$\",\n            servesCuisine: [\"Italian\", \"Mediterranean\"],\n            menu: \"https://example.com/restaurants/luigis/menu\",\n            aggregateRating: {\n              ratingValue: 4.7,\n              bestRating: 5,\n              ratingCount: 892,\n            },\n            review: {\n              reviewRating: { ratingValue: 5 },\n              author: \"Food & Wine Magazine\",\n              reviewBody:\n                \"Authentic Italian cuisine with a modern twist. Outstanding pasta and wine selection.\",\n            },\n            geo: {\n              latitude: 40.7489,\n              longitude: -73.968,\n            },\n            openingHoursSpecification: [\n              {\n                dayOfWeek: [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\"],\n                opens: \"11:30\",\n                closes: \"22:00\",\n              },\n              {\n                dayOfWeek: [\"Friday\", \"Saturday\"],\n                opens: \"11:30\",\n                closes: \"23:00\",\n              },\n              {\n                dayOfWeek: \"Sunday\",\n                opens: \"12:00\",\n                closes: \"21:00\",\n              },\n            ],\n            sameAs: [\n              \"https://www.facebook.com/luigisbistro\",\n              \"https://www.instagram.com/luigisbistro\",\n            ],\n          },\n          {\n            name: \"Sakura Sushi House\",\n            address: {\n              streetAddress: \"456 Oak Avenue\",\n              addressLocality: \"San Francisco\",\n              addressRegion: \"CA\",\n              postalCode: \"94102\",\n              addressCountry: \"US\",\n            },\n            telephone: \"+1-415-555-0200\",\n            url: \"https://example.com/restaurants/sakura\",\n            image: \"https://example.com/sakura-sushi.jpg\",\n            priceRange: \"$$\",\n            servesCuisine: [\"Japanese\", \"Sushi\"],\n            menu: \"https://example.com/restaurants/sakura/menu\",\n            aggregateRating: {\n              ratingValue: 4.8,\n              ratingCount: 1250,\n            },\n            geo: {\n              latitude: 37.7749,\n              longitude: -122.4194,\n            },\n            openingHoursSpecification: {\n              dayOfWeek: [\n                \"Monday\",\n                \"Tuesday\",\n                \"Wednesday\",\n                \"Thursday\",\n                \"Friday\",\n                \"Saturday\",\n              ],\n              opens: \"11:00\",\n              closes: \"22:30\",\n            },\n          },\n          {\n            name: \"The Garden Terrace\",\n            address: \"789 Park Lane, Chicago, IL 60601\",\n            telephone: \"+1-312-555-0300\",\n            image: {\n              url: \"https://example.com/garden-terrace.jpg\",\n              width: 1200,\n              height: 800,\n            },\n            priceRange: \"$$$$\",\n            servesCuisine: [\"French\", \"Contemporary\", \"Vegetarian Options\"],\n            aggregateRating: {\n              ratingValue: 4.9,\n              ratingCount: 567,\n            },\n            review: [\n              {\n                reviewRating: { ratingValue: 5, bestRating: 5 },\n                author: \"Michelin Guide\",\n                reviewBody:\n                  \"Exceptional fine dining experience with innovative seasonal menus\",\n                datePublished: \"2024-01-20\",\n              },\n              {\n                reviewRating: { ratingValue: 5 },\n                author: {\n                  name: \"James Food Critic\",\n                  url: \"https://example.com/critics/james\",\n                },\n                reviewBody: \"A culinary masterpiece in every dish\",\n              },\n            ],\n            geo: {\n              latitude: 41.8781,\n              longitude: -87.6298,\n            },\n            openingHoursSpecification: [\n              {\n                dayOfWeek: [\"Tuesday\", \"Wednesday\", \"Thursday\"],\n                opens: \"17:00\",\n                closes: \"22:00\",\n              },\n              {\n                dayOfWeek: [\"Friday\", \"Saturday\"],\n                opens: \"17:00\",\n                closes: \"23:30\",\n              },\n            ],\n          },\n          {\n            name: \"Taco Paradise\",\n            address: \"321 Sunset Boulevard, Los Angeles, CA 90028\",\n            telephone: \"+1-323-555-0400\",\n            url: \"https://example.com/restaurants/taco-paradise\",\n            image: [\n              \"https://example.com/taco-paradise-1.jpg\",\n              \"https://example.com/taco-paradise-2.jpg\",\n              \"https://example.com/taco-paradise-3.jpg\",\n            ],\n            priceRange: \"$\",\n            servesCuisine: \"Mexican\",\n            aggregateRating: {\n              ratingValue: 4.6,\n              ratingCount: 2341,\n            },\n            openingHoursSpecification: {\n              dayOfWeek: [\n                \"Monday\",\n                \"Tuesday\",\n                \"Wednesday\",\n                \"Thursday\",\n                \"Friday\",\n                \"Saturday\",\n                \"Sunday\",\n              ],\n              opens: \"10:00\",\n              closes: \"23:00\",\n            },\n          },\n        ]}\n      />\n\n      <h1 className=\"text-4xl font-bold mb-8\">Restaurant Carousel Example</h1>\n\n      <div className=\"prose lg:prose-xl\">\n        <p>\n          Discover top-rated restaurants in major cities across the United\n          States.\n        </p>\n\n        <h2>Featured Restaurants</h2>\n\n        <div className=\"grid gap-6 mt-6\">\n          <div className=\"border p-6 rounded-lg shadow\">\n            <div className=\"flex gap-6\">\n              <div className=\"w-32 h-32 bg-gray-200 rounded flex-shrink-0\"></div>\n              <div className=\"flex-1\">\n                <h3 className=\"text-2xl font-semibold\">\n                  Luigi's Italian Bistro\n                </h3>\n                <p className=\"text-gray-600 mt-1\">\n                  Italian, Mediterranean • $$$ • New York, NY\n                </p>\n                <div className=\"flex items-center gap-4 mt-2\">\n                  <span className=\"text-yellow-500\">★ 4.7/5</span>\n                  <span className=\"text-gray-500 text-sm\">(892 reviews)</span>\n                </div>\n                <p className=\"text-gray-600 mt-3\">\n                  Authentic Italian cuisine with a modern twist. Outstanding\n                  pasta and wine selection.\n                </p>\n                <div className=\"mt-3 text-sm text-gray-600\">\n                  <p>📍 123 Main Street, New York, NY 10001</p>\n                  <p>📞 +1-212-555-0100</p>\n                  <p>\n                    🕐 Mon-Thu: 11:30-22:00, Fri-Sat: 11:30-23:00, Sun:\n                    12:00-21:00\n                  </p>\n                </div>\n                <div className=\"mt-4 flex gap-3\">\n                  <a\n                    href=\"https://example.com/restaurants/luigis\"\n                    className=\"text-blue-600\"\n                  >\n                    Visit Website →\n                  </a>\n                  <a\n                    href=\"https://example.com/restaurants/luigis/menu\"\n                    className=\"text-blue-600\"\n                  >\n                    View Menu →\n                  </a>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"border p-6 rounded-lg shadow\">\n            <div className=\"flex gap-6\">\n              <div className=\"w-32 h-32 bg-gray-200 rounded flex-shrink-0\"></div>\n              <div className=\"flex-1\">\n                <h3 className=\"text-2xl font-semibold\">Sakura Sushi House</h3>\n                <p className=\"text-gray-600 mt-1\">\n                  Japanese, Sushi • $$ • San Francisco, CA\n                </p>\n                <div className=\"flex items-center gap-4 mt-2\">\n                  <span className=\"text-yellow-500\">★ 4.8/5</span>\n                  <span className=\"text-gray-500 text-sm\">(1,250 reviews)</span>\n                </div>\n                <p className=\"text-gray-600 mt-3\">\n                  Fresh, authentic sushi and Japanese cuisine in the heart of\n                  San Francisco.\n                </p>\n                <div className=\"mt-3 text-sm text-gray-600\">\n                  <p>📍 456 Oak Avenue, San Francisco, CA 94102</p>\n                  <p>📞 +1-415-555-0200</p>\n                  <p>🕐 Mon-Sat: 11:00-22:30, Sun: Closed</p>\n                </div>\n                <div className=\"mt-4 flex gap-3\">\n                  <a\n                    href=\"https://example.com/restaurants/sakura\"\n                    className=\"text-blue-600\"\n                  >\n                    Visit Website →\n                  </a>\n                  <a\n                    href=\"https://example.com/restaurants/sakura/menu\"\n                    className=\"text-blue-600\"\n                  >\n                    View Menu →\n                  </a>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"border p-6 rounded-lg shadow\">\n            <div className=\"flex gap-6\">\n              <div className=\"w-32 h-32 bg-gray-200 rounded flex-shrink-0\"></div>\n              <div className=\"flex-1\">\n                <h3 className=\"text-2xl font-semibold\">The Garden Terrace</h3>\n                <p className=\"text-gray-600 mt-1\">\n                  French, Contemporary • $$$$ • Chicago, IL\n                </p>\n                <div className=\"flex items-center gap-4 mt-2\">\n                  <span className=\"text-yellow-500\">★ 4.9/5</span>\n                  <span className=\"text-gray-500 text-sm\">(567 reviews)</span>\n                  <span className=\"bg-yellow-100 text-yellow-800 text-xs px-2 py-1 rounded\">\n                    Michelin Guide\n                  </span>\n                </div>\n                <p className=\"text-gray-600 mt-3\">\n                  Exceptional fine dining experience with innovative seasonal\n                  menus.\n                </p>\n                <div className=\"mt-3 text-sm text-gray-600\">\n                  <p>📍 789 Park Lane, Chicago, IL 60601</p>\n                  <p>📞 +1-312-555-0300</p>\n                  <p>🕐 Tue-Thu: 17:00-22:00, Fri-Sat: 17:00-23:30</p>\n                </div>\n                <button className=\"bg-blue-600 text-white px-4 py-2 rounded mt-4\">\n                  Reserve Table\n                </button>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"border p-6 rounded-lg shadow\">\n            <div className=\"flex gap-6\">\n              <div className=\"w-32 h-32 bg-gray-200 rounded flex-shrink-0\"></div>\n              <div className=\"flex-1\">\n                <h3 className=\"text-2xl font-semibold\">Taco Paradise</h3>\n                <p className=\"text-gray-600 mt-1\">\n                  Mexican • $ • Los Angeles, CA\n                </p>\n                <div className=\"flex items-center gap-4 mt-2\">\n                  <span className=\"text-yellow-500\">★ 4.6/5</span>\n                  <span className=\"text-gray-500 text-sm\">(2,341 reviews)</span>\n                </div>\n                <p className=\"text-gray-600 mt-3\">\n                  Authentic Mexican street food with the best tacos in LA.\n                </p>\n                <div className=\"mt-3 text-sm text-gray-600\">\n                  <p>📍 321 Sunset Boulevard, Los Angeles, CA 90028</p>\n                  <p>📞 +1-323-555-0400</p>\n                  <p>🕐 Daily: 10:00-23:00</p>\n                </div>\n                <a\n                  href=\"https://example.com/restaurants/taco-paradise\"\n                  className=\"text-blue-600 mt-4 inline-block\"\n                >\n                  Order Online →\n                </a>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/carousel-summary/page.tsx",
    "content": "import { CarouselJsonLd } from \"next-seo\";\n\nexport default function CarouselSummaryPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <CarouselJsonLd\n        urls={[\n          \"https://example.com/recipe/chocolate-cookies\",\n          \"https://example.com/recipe/banana-bread\",\n          { url: \"https://example.com/recipe/apple-pie\", position: 3 },\n          \"https://example.com/recipe/pancakes\",\n          { url: \"https://example.com/recipe/brownies\", position: 5 },\n        ]}\n      />\n\n      <h1 className=\"text-4xl font-bold mb-8\">Carousel Summary Page Example</h1>\n\n      <div className=\"prose lg:prose-xl\">\n        <p>\n          This page demonstrates the summary page pattern for carousels, where\n          we only provide URLs to detail pages.\n        </p>\n\n        <h2>Best Recipes Collection</h2>\n\n        <div className=\"grid gap-4 mt-6\">\n          <div className=\"border p-4 rounded\">\n            <h3>Chocolate Chip Cookies</h3>\n            <p>Classic chocolate chip cookies that everyone loves.</p>\n            <a\n              href=\"https://example.com/recipe/chocolate-cookies\"\n              className=\"text-blue-600\"\n            >\n              View Recipe →\n            </a>\n          </div>\n\n          <div className=\"border p-4 rounded\">\n            <h3>Banana Bread</h3>\n            <p>Moist and delicious banana bread recipe.</p>\n            <a\n              href=\"https://example.com/recipe/banana-bread\"\n              className=\"text-blue-600\"\n            >\n              View Recipe →\n            </a>\n          </div>\n\n          <div className=\"border p-4 rounded\">\n            <h3>Apple Pie</h3>\n            <p>Traditional American apple pie with a flaky crust.</p>\n            <a\n              href=\"https://example.com/recipe/apple-pie\"\n              className=\"text-blue-600\"\n            >\n              View Recipe →\n            </a>\n          </div>\n\n          <div className=\"border p-4 rounded\">\n            <h3>Fluffy Pancakes</h3>\n            <p>Perfect pancakes for a weekend breakfast.</p>\n            <a\n              href=\"https://example.com/recipe/pancakes\"\n              className=\"text-blue-600\"\n            >\n              View Recipe →\n            </a>\n          </div>\n\n          <div className=\"border p-4 rounded\">\n            <h3>Fudge Brownies</h3>\n            <p>Rich and chocolatey brownies with a perfect texture.</p>\n            <a\n              href=\"https://example.com/recipe/brownies\"\n              className=\"text-blue-600\"\n            >\n              View Recipe →\n            </a>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/claim-review/page.tsx",
    "content": "import { ClaimReviewJsonLd } from \"next-seo\";\n\nexport default function ClaimReviewPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ClaimReviewJsonLd\n        claimReviewed=\"The world is flat\"\n        reviewRating={{\n          ratingValue: 1,\n          bestRating: 5,\n          worstRating: 1,\n          alternateName: \"False\",\n        }}\n        url=\"https://example.com/news/science/worldisflat.html\"\n        author=\"Example.com science watch\"\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Fact Check: The World is Flat</h1>\n        <div className=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4\">\n          <strong className=\"font-bold\">Rating: False</strong>\n        </div>\n        <p>\n          We fact-checked the claim that \"the world is flat\" and found it to be\n          demonstrably false based on overwhelming scientific evidence.\n        </p>\n        <h2>The Claim</h2>\n        <p>\n          Various online communities continue to promote the idea that Earth is\n          flat rather than spherical, despite centuries of scientific evidence\n          to the contrary.\n        </p>\n        <h2>The Facts</h2>\n        <p>The Earth is an oblate spheroid, as proven by:</p>\n        <ul>\n          <li>Satellite imagery from space</li>\n          <li>Ships disappearing hull-first over the horizon</li>\n          <li>\n            Different star constellations visible from different latitudes\n          </li>\n          <li>Time zones and the day/night cycle</li>\n          <li>Gravity measurements across the globe</li>\n        </ul>\n        <h2>Our Verdict</h2>\n        <p>\n          The claim that Earth is flat is false. This has been conclusively\n          proven through multiple lines of scientific evidence spanning\n          centuries.\n        </p>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/claim-review-advanced/page.tsx",
    "content": "import { ClaimReviewJsonLd } from \"next-seo\";\n\nexport default function ClaimReviewAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ClaimReviewJsonLd\n        claimReviewed=\"The world is flat\"\n        reviewRating={{\n          ratingValue: 1,\n          bestRating: 5,\n          worstRating: 1,\n          alternateName: \"False\",\n          name: \"False\",\n        }}\n        url=\"https://example.com/news/science/worldisflat.html\"\n        author={{\n          name: \"Example.com Science Watch\",\n          url: \"https://example.com/science\",\n          logo: \"https://example.com/logo.jpg\",\n        }}\n        itemReviewed={{\n          author: {\n            \"@type\": \"Organization\",\n            name: \"Square World Society\",\n            sameAs:\n              \"https://example.flatworlders.com/we-know-that-the-world-is-flat\",\n          },\n          datePublished: \"2024-06-20\",\n          appearance: {\n            \"@type\": \"OpinionNewsArticle\",\n            url: \"https://example.com/news/a122121\",\n            headline: \"Square Earth - Flat earthers for the Internet age\",\n            datePublished: \"2024-06-22\",\n            author: {\n              name: \"T. Tellar\",\n            },\n            image: \"https://example.com/photos/1x1/photo.jpg\",\n            publisher: {\n              name: \"Skeptical News\",\n              logo: {\n                url: \"https://example.com/logo.jpg\",\n              },\n            },\n          },\n        }}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Fact Check: The World is Flat (Advanced Example)</h1>\n        <div className=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4\">\n          <strong className=\"font-bold\">Rating: False (1/5)</strong>\n        </div>\n        <div className=\"bg-gray-100 p-4 rounded mb-4\">\n          <h3 className=\"font-bold\">Claim Details:</h3>\n          <p>\n            <strong>Claimed by:</strong> Square World Society\n          </p>\n          <p>\n            <strong>First published:</strong> June 20, 2024\n          </p>\n          <p>\n            <strong>Appeared in:</strong> \"Square Earth - Flat earthers for the\n            Internet age\" by T. Tellar\n          </p>\n        </div>\n        <p>\n          This advanced example demonstrates a fact check with complete claim\n          tracking, including the original source, publication details, and\n          appearance information.\n        </p>\n        <h2>About This Fact Check</h2>\n        <p>\n          This fact check was conducted by Example.com Science Watch, an\n          independent fact-checking organization dedicated to verifying\n          scientific claims.\n        </p>\n        <h2>The Claim Origin</h2>\n        <p>\n          The claim was originally made by the Square World Society on June 20,\n          2024, and subsequently appeared in an opinion piece titled \"Square\n          Earth - Flat earthers for the Internet age\" published by Skeptical\n          News on June 22, 2024.\n        </p>\n        <h2>Our Analysis</h2>\n        <p>\n          After thorough investigation and consultation with multiple scientific\n          experts, we rate this claim as FALSE. The evidence overwhelmingly\n          supports that Earth is spherical, not flat.\n        </p>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/claim-review-organization/page.tsx",
    "content": "import { ClaimReviewJsonLd } from \"next-seo\";\n\nexport default function ClaimReviewOrganizationPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ClaimReviewJsonLd\n        claimReviewed=\"Climate change is not real\"\n        reviewRating={{\n          ratingValue: 1,\n          bestRating: 5,\n          worstRating: 1,\n          alternateName: \"Pants on Fire\",\n        }}\n        url=\"https://example.com/fact-check/climate-change-denial\"\n        author={{\n          name: \"Climate Facts Organization\",\n          url: \"https://example.com\",\n          logo: {\n            url: \"https://example.com/logo.png\",\n            width: 300,\n            height: 60,\n          },\n          sameAs: [\n            \"https://twitter.com/climatefacts\",\n            \"https://facebook.com/climatefacts\",\n          ],\n        }}\n        itemReviewed={{\n          author: {\n            \"@type\": \"Organization\",\n            name: \"Climate Denial Institute\",\n            url: \"https://example-denial.com\",\n          },\n          datePublished: \"2024-07-01\",\n          firstAppearance: {\n            url: \"https://example-denial.com/climate-hoax\",\n            headline: \"The Great Climate Hoax Exposed\",\n            datePublished: \"2024-07-01\",\n            author: {\n              \"@type\": \"Organization\",\n              name: \"Climate Denial Institute\",\n              url: \"https://example-denial.com\",\n            },\n          },\n        }}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Fact Check: \"Climate change is not real\"</h1>\n        <div className=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4\">\n          <strong className=\"font-bold\">Rating: Pants on Fire</strong>\n          <span className=\"block text-sm\">\n            Completely and demonstrably false\n          </span>\n        </div>\n        <div className=\"bg-blue-50 p-4 rounded mb-4\">\n          <h3 className=\"font-bold\">About the Fact Checker:</h3>\n          <p>\n            <strong>Organization:</strong> Climate Facts Organization\n          </p>\n          <p>\n            <strong>Focus:</strong> Environmental and climate science claims\n          </p>\n        </div>\n        <h2>The Claim</h2>\n        <p>\n          The Climate Denial Institute claimed on July 1, 2024, that \"climate\n          change is not real\" in their article \"The Great Climate Hoax Exposed.\"\n        </p>\n        <h2>Why This is False</h2>\n        <p>\n          This claim contradicts the overwhelming scientific consensus supported\n          by:\n        </p>\n        <ul>\n          <li>\n            97% of climate scientists agree climate change is real and\n            human-caused\n          </li>\n          <li>\n            Temperature records from NASA, NOAA, and meteorological\n            organizations worldwide\n          </li>\n          <li>\n            Observable effects including melting ice caps, rising sea levels,\n            and extreme weather events\n          </li>\n          <li>\n            Peer-reviewed studies from thousands of independent researchers\n          </li>\n        </ul>\n        <h2>About Climate Facts Organization</h2>\n        <p>\n          Climate Facts Organization is an independent fact-checking\n          organization specializing in environmental and climate science claims.\n          We work with climate scientists and researchers to verify claims about\n          climate change.\n        </p>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/course/page.tsx",
    "content": "import { CourseJsonLd } from \"next-seo\";\n\nexport default function CoursePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <CourseJsonLd\n        name=\"Introduction to Computer Science and Programming\"\n        description=\"This is an introductory CS course laying out the basics.\"\n        url=\"https://example.com/courses/intro-cs\"\n        provider={{\n          name: \"University of Technology - Eureka\",\n          sameAs: \"https://www.example.com\",\n        }}\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-4\">\n          Introduction to Computer Science and Programming\n        </h1>\n\n        <div className=\"mb-6\">\n          <p className=\"text-gray-600\">\n            Offered by{\" \"}\n            <a\n              href=\"https://www.example.com\"\n              className=\"text-blue-600 hover:underline\"\n            >\n              University of Technology - Eureka\n            </a>\n          </p>\n        </div>\n\n        <div className=\"prose lg:prose-xl\">\n          <h2>Course Description</h2>\n          <p>\n            This is an introductory CS course laying out the basics. You'll\n            learn fundamental programming concepts, algorithms, and\n            problem-solving techniques that form the foundation of computer\n            science.\n          </p>\n\n          <h2>What You'll Learn</h2>\n          <ul>\n            <li>Basic programming concepts and syntax</li>\n            <li>Data structures and algorithms</li>\n            <li>Problem-solving techniques</li>\n            <li>Software design principles</li>\n            <li>Debugging and testing methodologies</li>\n          </ul>\n\n          <h2>Prerequisites</h2>\n          <p>\n            No prior programming experience required. Basic mathematics\n            knowledge (algebra) is helpful but not mandatory.\n          </p>\n\n          <h2>Course Format</h2>\n          <p>\n            This course consists of video lectures, hands-on programming\n            assignments, quizzes, and a final project. Expected time commitment\n            is 8-10 hours per week over 12 weeks.\n          </p>\n        </div>\n\n        <div className=\"mt-8\">\n          <button className=\"bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700\">\n            Enroll Now\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/course-list/page.tsx",
    "content": "import { CourseJsonLd } from \"next-seo\";\n\ninterface CourseProvider {\n  name: string;\n  sameAs: string;\n}\n\ninterface Course {\n  name: string;\n  description: string;\n  url?: string;\n  provider: CourseProvider;\n}\n\nexport default function CourseListPage() {\n  const courses: Course[] = [\n    {\n      name: \"Introduction to Computer Science and Programming\",\n      description: \"This is an introductory CS course laying out the basics.\",\n      url: \"https://example.com/courses#intro-to-cs\",\n      provider: {\n        name: \"University of Technology - Example\",\n        sameAs: \"https://www.example.com\",\n      },\n    },\n    {\n      name: \"Intermediate Computer Science and Programming\",\n      description: \"This CS course builds on the basics from the intro course.\",\n      url: \"https://example.com/courses#intermediate-cs\",\n      provider: {\n        name: \"University of Technology - Example\",\n        sameAs: \"https://www.example.com\",\n      },\n    },\n    {\n      name: \"Advanced Computer Science and Programming\",\n      description: \"This CS course covers advanced programming principles.\",\n      url: \"https://example.com/courses#advanced-cs\",\n      provider: {\n        name: \"University of Technology - Eureka\",\n        sameAs: \"https://www.example.com\",\n      },\n    },\n  ];\n\n  return (\n    <div className=\"container mx-auto p-8\">\n      <CourseJsonLd type=\"list\" courses={courses} />\n\n      <div className=\"max-w-6xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-8\">Computer Science Courses</h1>\n\n        <p className=\"text-lg text-gray-600 mb-8\">\n          Explore our comprehensive computer science curriculum, from\n          introductory concepts to advanced programming techniques.\n        </p>\n\n        <div className=\"grid gap-6\">\n          {courses.map((course, index) => (\n            <div\n              key={index}\n              id={course.url?.split(\"#\")[1]}\n              className=\"border rounded-lg p-6 hover:shadow-lg transition-shadow\"\n            >\n              <h2 className=\"text-2xl font-semibold mb-2\">\n                <a\n                  href={course.url}\n                  className=\"text-blue-600 hover:text-blue-800\"\n                >\n                  {course.name}\n                </a>\n              </h2>\n\n              <p className=\"text-gray-600 mb-3\">{course.description}</p>\n\n              <div className=\"text-sm text-gray-500\">\n                Offered by{\" \"}\n                <a\n                  href={course.provider.sameAs}\n                  className=\"text-blue-600 hover:underline\"\n                >\n                  {course.provider.name}\n                </a>\n              </div>\n\n              <div className=\"mt-4 flex gap-4\">\n                <button className=\"bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700\">\n                  View Details\n                </button>\n                <button className=\"border border-blue-600 text-blue-600 px-4 py-2 rounded hover:bg-blue-50\">\n                  Save for Later\n                </button>\n              </div>\n            </div>\n          ))}\n        </div>\n\n        <div className=\"mt-12 bg-gray-100 p-6 rounded-lg\">\n          <h2 className=\"text-xl font-semibold mb-3\">\n            Why Study Computer Science?\n          </h2>\n          <p className=\"text-gray-700\">\n            Computer Science is at the heart of modern innovation. Our courses\n            provide a solid foundation in computational thinking,\n            problem-solving, and software development that will prepare you for\n            a rewarding career in technology.\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/course-list-summary/page.tsx",
    "content": "import { CourseJsonLd } from \"next-seo\";\n\nexport default function CourseListSummaryPage() {\n  const courseUrls = [\n    \"https://example.com/courses/intro-programming\",\n    \"https://example.com/courses/web-development\",\n    \"https://example.com/courses/data-science\",\n    \"https://example.com/courses/machine-learning\",\n    \"https://example.com/courses/mobile-development\",\n  ];\n\n  return (\n    <div className=\"container mx-auto p-8\">\n      <CourseJsonLd type=\"list\" urls={courseUrls} />\n\n      <div className=\"max-w-6xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-8\">Browse Our Courses</h1>\n\n        <p className=\"text-lg text-gray-600 mb-8\">\n          Discover a wide range of technology courses designed to advance your\n          career and expand your knowledge.\n        </p>\n\n        <div className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-6\">\n          <a\n            href=\"https://example.com/courses/intro-programming\"\n            className=\"block border rounded-lg p-6 hover:shadow-lg transition-shadow\"\n          >\n            <h2 className=\"text-xl font-semibold mb-2\">\n              Introduction to Programming\n            </h2>\n            <p className=\"text-gray-600 mb-4\">\n              Start your coding journey with fundamental programming concepts.\n            </p>\n            <span className=\"text-blue-600 hover:text-blue-800\">\n              Learn more →\n            </span>\n          </a>\n\n          <a\n            href=\"https://example.com/courses/web-development\"\n            className=\"block border rounded-lg p-6 hover:shadow-lg transition-shadow\"\n          >\n            <h2 className=\"text-xl font-semibold mb-2\">\n              Web Development Bootcamp\n            </h2>\n            <p className=\"text-gray-600 mb-4\">\n              Build modern web applications from scratch.\n            </p>\n            <span className=\"text-blue-600 hover:text-blue-800\">\n              Learn more →\n            </span>\n          </a>\n\n          <a\n            href=\"https://example.com/courses/data-science\"\n            className=\"block border rounded-lg p-6 hover:shadow-lg transition-shadow\"\n          >\n            <h2 className=\"text-xl font-semibold mb-2\">\n              Data Science Fundamentals\n            </h2>\n            <p className=\"text-gray-600 mb-4\">\n              Master data analysis and visualization techniques.\n            </p>\n            <span className=\"text-blue-600 hover:text-blue-800\">\n              Learn more →\n            </span>\n          </a>\n\n          <a\n            href=\"https://example.com/courses/machine-learning\"\n            className=\"block border rounded-lg p-6 hover:shadow-lg transition-shadow\"\n          >\n            <h2 className=\"text-xl font-semibold mb-2\">\n              Machine Learning & AI\n            </h2>\n            <p className=\"text-gray-600 mb-4\">\n              Explore artificial intelligence and ML algorithms.\n            </p>\n            <span className=\"text-blue-600 hover:text-blue-800\">\n              Learn more →\n            </span>\n          </a>\n\n          <a\n            href=\"https://example.com/courses/mobile-development\"\n            className=\"block border rounded-lg p-6 hover:shadow-lg transition-shadow\"\n          >\n            <h2 className=\"text-xl font-semibold mb-2\">\n              Mobile App Development\n            </h2>\n            <p className=\"text-gray-600 mb-4\">\n              Create native and cross-platform mobile applications.\n            </p>\n            <span className=\"text-blue-600 hover:text-blue-800\">\n              Learn more →\n            </span>\n          </a>\n        </div>\n\n        <div className=\"mt-12 text-center\">\n          <p className=\"text-gray-600 mb-4\">\n            Can't find what you're looking for?\n          </p>\n          <button className=\"bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700\">\n            View All Courses\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/creative-work/page.tsx",
    "content": "import { CreativeWorkJsonLd } from \"next-seo\";\n\nexport default function CreativeWorkPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <CreativeWorkJsonLd\n        type=\"Article\"\n        headline=\"Premium Article: Understanding Paywalled Content\"\n        url=\"https://example.com/articles/premium-content\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        dateModified=\"2024-01-02T10:00:00+00:00\"\n        author=\"Sarah Johnson\"\n        image=\"https://example.com/images/premium-article.jpg\"\n        publisher={{\n          name: \"Premium Publications\",\n          logo: \"https://example.com/logo.png\",\n        }}\n        description=\"This article demonstrates how to mark paywalled content with structured data\"\n        isAccessibleForFree={false}\n        hasPart={{\n          isAccessibleForFree: false,\n          cssSelector: \".paywall\",\n        }}\n        mainEntityOfPage=\"https://example.com/articles\"\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Premium Article: Understanding Paywalled Content</h1>\n\n        <div className=\"non-paywall bg-white p-6 rounded-lg shadow-md\">\n          <h2>Free Preview</h2>\n          <p>\n            This is the free preview section that everyone can read. It provides\n            a glimpse into the premium content that follows.\n          </p>\n          <p>\n            Structured data helps search engines understand which parts of your\n            content require a subscription or payment, distinguishing legitimate\n            paywalled content from deceptive cloaking practices.\n          </p>\n        </div>\n\n        <div className=\"paywall bg-yellow-50 p-6 rounded-lg shadow-md mt-6 border-2 border-yellow-300\">\n          <h2>Premium Content</h2>\n          <p className=\"text-gray-600 italic\">\n            This section is marked as paywalled content using the cssSelector\n            \".paywall\"\n          </p>\n          <p>\n            This premium content would typically require a subscription to\n            access. The structured data marks this section with\n            isAccessibleForFree: false and uses the CSS selector to identify the\n            paywalled portion.\n          </p>\n          <p>\n            Search engines can now properly understand that this content\n            requires payment or subscription, which helps maintain trust with\n            users while allowing the content to be properly indexed.\n          </p>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/creative-work-blog/page.tsx",
    "content": "import { CreativeWorkJsonLd } from \"next-seo\";\n\nexport default function CreativeWorkBlogPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <CreativeWorkJsonLd\n        type=\"Blog\"\n        name=\"Premium Tech Insights Blog\"\n        url=\"https://example.com/blog\"\n        description=\"A premium technology blog with exclusive content for subscribers\"\n        author={{\n          name: \"Tech Insights Team\",\n          url: \"https://example.com/team\",\n          logo: \"https://example.com/team-logo.png\",\n        }}\n        publisher={{\n          name: \"Tech Insights Publishing\",\n          logo: \"https://example.com/blog-logo.png\",\n        }}\n        datePublished=\"2024-01-01T00:00:00+00:00\"\n        isAccessibleForFree={false}\n        hasPart={{\n          isAccessibleForFree: false,\n          cssSelector: \".members-only\",\n        }}\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <header className=\"text-center mb-8\">\n          <h1 className=\"text-4xl font-bold\">Premium Tech Insights Blog</h1>\n          <p className=\"text-lg text-gray-600 mt-2\">\n            Exclusive technology insights for our subscribers\n          </p>\n        </header>\n\n        <div className=\"blog-intro bg-white p-6 rounded-lg shadow-md mb-6\">\n          <h2>Welcome to Tech Insights</h2>\n          <p>\n            Our blog provides in-depth analysis of the latest technology trends,\n            expert opinions, and exclusive interviews with industry leaders.\n          </p>\n          <p>\n            While some content is freely available, our premium articles are\n            reserved for subscribers who support our independent journalism.\n          </p>\n        </div>\n\n        <div className=\"recent-posts\">\n          <h2 className=\"text-2xl font-bold mb-4\">Recent Posts</h2>\n\n          <article className=\"bg-white p-6 rounded-lg shadow-md mb-4\">\n            <h3 className=\"text-xl font-semibold\">\n              Free Article: Introduction to AI Ethics\n            </h3>\n            <p className=\"text-gray-600\">\n              An overview of ethical considerations in artificial intelligence\n              development.\n            </p>\n            <p className=\"text-sm text-gray-500 mt-2\">\n              Published: January 10, 2024\n            </p>\n          </article>\n\n          <article className=\"members-only bg-gradient-to-r from-purple-50 to-pink-50 p-6 rounded-lg shadow-md mb-4 border-2 border-purple-300\">\n            <div className=\"flex items-center justify-between mb-2\">\n              <h3 className=\"text-xl font-semibold\">\n                The Future of Quantum Computing\n              </h3>\n              <span className=\"bg-purple-600 text-white px-3 py-1 rounded-full text-sm\">\n                Members Only\n              </span>\n            </div>\n            <p className=\"text-gray-600 italic\">\n              This premium content is marked with cssSelector \".members-only\"\n            </p>\n            <p className=\"text-gray-700 mt-2\">\n              An exclusive deep dive into quantum computing breakthroughs and\n              their potential impact on cryptography, drug discovery, and\n              artificial intelligence over the next decade.\n            </p>\n            <p className=\"text-sm text-gray-500 mt-2\">\n              Published: January 12, 2024\n            </p>\n          </article>\n\n          <article className=\"members-only bg-gradient-to-r from-blue-50 to-cyan-50 p-6 rounded-lg shadow-md mb-4 border-2 border-blue-300\">\n            <div className=\"flex items-center justify-between mb-2\">\n              <h3 className=\"text-xl font-semibold\">\n                Interview: CEO of Neural Dynamics\n              </h3>\n              <span className=\"bg-blue-600 text-white px-3 py-1 rounded-full text-sm\">\n                Members Only\n              </span>\n            </div>\n            <p className=\"text-gray-600 italic\">\n              This premium content is marked with cssSelector \".members-only\"\n            </p>\n            <p className=\"text-gray-700 mt-2\">\n              Exclusive interview with Dr. Michael Chen about the latest\n              developments in brain-computer interfaces and the future of\n              human-machine interaction.\n            </p>\n            <p className=\"text-sm text-gray-500 mt-2\">\n              Published: January 14, 2024\n            </p>\n          </article>\n\n          <article className=\"bg-white p-6 rounded-lg shadow-md mb-4\">\n            <h3 className=\"text-xl font-semibold\">\n              Free Article: Getting Started with Web3\n            </h3>\n            <p className=\"text-gray-600\">\n              A beginner's guide to understanding blockchain and decentralized\n              applications.\n            </p>\n            <p className=\"text-sm text-gray-500 mt-2\">\n              Published: January 8, 2024\n            </p>\n          </article>\n        </div>\n\n        <div className=\"subscription-cta bg-gradient-to-r from-indigo-500 to-purple-600 text-white p-8 rounded-lg shadow-lg mt-8\">\n          <h2 className=\"text-2xl font-bold mb-4\">Become a Member</h2>\n          <p className=\"mb-4\">\n            Get unlimited access to all premium articles, exclusive newsletters,\n            and early access to new content.\n          </p>\n          <button className=\"bg-white text-indigo-600 px-6 py-3 rounded-lg font-semibold hover:bg-gray-100 transition\">\n            Subscribe Now\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/creative-work-multiple/page.tsx",
    "content": "import { CreativeWorkJsonLd } from \"next-seo\";\n\nexport default function CreativeWorkMultiplePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <CreativeWorkJsonLd\n        type=\"Article\"\n        headline=\"In-Depth Analysis: Multiple Premium Sections\"\n        url=\"https://example.com/articles/in-depth-analysis\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        dateModified=\"2024-01-03T14:30:00+00:00\"\n        author={[\n          \"Dr. Emily Chen\",\n          {\n            name: \"Research Institute\",\n            logo: \"https://example.com/institute-logo.png\",\n          },\n        ]}\n        image={[\n          \"https://example.com/images/analysis-16x9.jpg\",\n          \"https://example.com/images/analysis-4x3.jpg\",\n          \"https://example.com/images/analysis-1x1.jpg\",\n        ]}\n        publisher={{\n          name: \"Academic Publishers Inc.\",\n          logo: \"https://example.com/publisher-logo.png\",\n        }}\n        description=\"A comprehensive analysis with multiple premium sections for subscribers\"\n        isAccessibleForFree={false}\n        hasPart={[\n          {\n            isAccessibleForFree: false,\n            cssSelector: \".section1\",\n          },\n          {\n            isAccessibleForFree: false,\n            cssSelector: \".section2\",\n          },\n        ]}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>In-Depth Analysis: Multiple Premium Sections</h1>\n\n        <div className=\"introduction bg-white p-6 rounded-lg shadow-md\">\n          <h2>Introduction</h2>\n          <p>\n            This article demonstrates how to mark multiple paywalled sections\n            within a single piece of content. Each premium section is identified\n            by a unique CSS selector.\n          </p>\n          <p>\n            The free introduction gives readers an overview of what's covered in\n            the premium sections below.\n          </p>\n        </div>\n\n        <div className=\"section1 bg-blue-50 p-6 rounded-lg shadow-md mt-6 border-2 border-blue-300\">\n          <h2>Premium Section 1: Detailed Research</h2>\n          <p className=\"text-gray-600 italic\">\n            This section is marked with cssSelector \".section1\"\n          </p>\n          <p>\n            This first premium section contains detailed research findings that\n            are available only to subscribers. The structured data identifies\n            this section as paywalled using the CSS class \"section1\".\n          </p>\n          <p>\n            Subscribers can access comprehensive data analysis, charts, and\n            expert insights in this section.\n          </p>\n        </div>\n\n        <div className=\"middle-content bg-white p-6 rounded-lg shadow-md mt-6\">\n          <h2>Free Interlude</h2>\n          <p>\n            This middle section is free content that bridges the two premium\n            sections. It helps maintain reader engagement while clearly\n            distinguishing between free and paid content areas.\n          </p>\n        </div>\n\n        <div className=\"section2 bg-purple-50 p-6 rounded-lg shadow-md mt-6 border-2 border-purple-300\">\n          <h2>Premium Section 2: Advanced Applications</h2>\n          <p className=\"text-gray-600 italic\">\n            This section is marked with cssSelector \".section2\"\n          </p>\n          <p>\n            The second premium section explores advanced applications and\n            real-world case studies. This content is also behind the paywall,\n            identified by the CSS class \"section2\".\n          </p>\n          <p>\n            Premium subscribers gain access to exclusive case studies,\n            implementation guides, and expert recommendations in this section.\n          </p>\n        </div>\n\n        <div className=\"conclusion bg-white p-6 rounded-lg shadow-md mt-6\">\n          <h2>Conclusion</h2>\n          <p>\n            The free conclusion summarizes the key points and encourages readers\n            to subscribe for full access to all premium sections.\n          </p>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/creative-work-news/page.tsx",
    "content": "import { CreativeWorkJsonLd } from \"next-seo\";\n\nexport default function CreativeWorkNewsPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <CreativeWorkJsonLd\n        type=\"NewsArticle\"\n        headline=\"Breaking: Major Scientific Discovery Behind Paywall\"\n        url=\"https://example.com/news/scientific-discovery\"\n        datePublished=\"2024-01-15T09:00:00+00:00\"\n        dateModified=\"2024-01-15T11:30:00+00:00\"\n        author={{\n          name: \"Jane Martinez\",\n          url: \"https://example.com/journalists/jane-martinez\",\n        }}\n        image={{\n          url: \"https://example.com/images/discovery-hero.jpg\",\n          width: 1200,\n          height: 630,\n          caption: \"Scientific breakthrough illustration\",\n        }}\n        publisher={{\n          name: \"Global News Network\",\n          logo: {\n            url: \"https://example.com/gnn-logo.png\",\n            width: 600,\n            height: 60,\n          },\n        }}\n        description=\"Breaking news about a major scientific discovery - full details available to subscribers\"\n        isAccessibleForFree={false}\n        hasPart={{\n          isAccessibleForFree: false,\n          cssSelector: \".premium-news\",\n        }}\n        mainEntityOfPage={{\n          \"@id\": \"https://example.com/news/scientific-discovery\",\n        }}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Breaking: Major Scientific Discovery Behind Paywall</h1>\n\n        <div className=\"lead bg-white p-6 rounded-lg shadow-md\">\n          <p className=\"text-lg font-semibold\">\n            Scientists at the International Research Center have announced a\n            groundbreaking discovery that could revolutionize our understanding\n            of quantum physics.\n          </p>\n          <p>\n            The discovery, made during a series of experiments over the past six\n            months, has implications for future technology development.\n          </p>\n          <p className=\"text-sm text-gray-600 mt-4\">\n            Published: January 15, 2024, 9:00 AM | Updated: 11:30 AM\n          </p>\n        </div>\n\n        <div className=\"premium-news bg-red-50 p-6 rounded-lg shadow-md mt-6 border-2 border-red-300\">\n          <div className=\"flex items-center mb-4\">\n            <svg\n              className=\"w-6 h-6 text-red-500 mr-2\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              viewBox=\"0 0 24 24\"\n            >\n              <path\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth={2}\n                d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"\n              />\n            </svg>\n            <span className=\"text-red-700 font-semibold\">\n              Subscriber Exclusive Content\n            </span>\n          </div>\n\n          <h2>Full Discovery Details</h2>\n          <p className=\"text-gray-600 italic\">\n            This premium section is marked with cssSelector \".premium-news\"\n          </p>\n\n          <h3>The Breakthrough</h3>\n          <p>\n            Dr. Sarah Chen and her team have successfully demonstrated quantum\n            entanglement at room temperature, a feat previously thought\n            impossible. The implications for quantum computing are enormous.\n          </p>\n\n          <h3>Technical Details</h3>\n          <p>\n            The experiment utilized a novel approach combining laser cooling\n            techniques with magnetic field manipulation. The team was able to\n            maintain entanglement for over 100 milliseconds at 25°C.\n          </p>\n\n          <h3>Future Applications</h3>\n          <p>\n            This discovery opens doors for practical quantum computers that\n            don't require extreme cooling, potentially making the technology\n            accessible for everyday use within the next decade.\n          </p>\n\n          <h3>Industry Response</h3>\n          <p>\n            Major tech companies have already expressed interest in licensing\n            the technology. \"This changes everything,\" said a spokesperson from\n            a leading quantum computing firm.\n          </p>\n        </div>\n\n        <div className=\"related bg-white p-6 rounded-lg shadow-md mt-6\">\n          <h2>Related Articles</h2>\n          <ul>\n            <li>Previous quantum computing breakthroughs</li>\n            <li>Interview with Dr. Sarah Chen</li>\n            <li>What this means for the future of technology</li>\n          </ul>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/custom-podcast/page.tsx",
    "content": "import { PodcastSeriesJsonLd } from \"../../components/custom/PodcastSeriesJsonLd\";\n\nexport default function CustomPodcastPage() {\n  return (\n    <div className=\"container\">\n      <PodcastSeriesJsonLd\n        name=\"Tech Talk Weekly\"\n        description=\"Weekly discussions about technology trends and innovations\"\n        host=\"Sarah Johnson\"\n        url=\"https://example.com/podcasts/tech-talk-weekly\"\n        image=\"https://example.com/podcast-cover.jpg\"\n        episodes={[\n          {\n            name: \"Episode 1: AI Revolution\",\n            duration: \"PT30M\",\n            datePublished: \"2024-01-01\",\n            description:\n              \"Exploring the latest developments in artificial intelligence\",\n            url: \"https://example.com/episodes/ep1-ai-revolution\",\n          },\n          {\n            name: \"Episode 2: Web3 Explained\",\n            duration: \"PT45M\",\n            datePublished: \"2024-01-08\",\n            description:\n              \"Demystifying blockchain and decentralized technologies\",\n            url: \"https://example.com/episodes/ep2-web3-explained\",\n          },\n          {\n            name: \"Episode 3: Quantum Computing\",\n            duration: \"PT35M\",\n            datePublished: \"2024-01-15\",\n            description:\n              \"The future of computing and its practical applications\",\n            url: \"https://example.com/episodes/ep3-quantum-computing\",\n          },\n        ]}\n      />\n\n      <main style={{ padding: \"2rem\", maxWidth: \"800px\", margin: \"0 auto\" }}>\n        <h1>Tech Talk Weekly</h1>\n        <p style={{ color: \"#666\", marginBottom: \"2rem\" }}>\n          Custom Component Demo - Built with next-seo processors\n        </p>\n\n        <section>\n          <h2>About the Podcast</h2>\n          <p>\n            This page demonstrates a custom PodcastSeries JSON-LD component\n            built using next-seo's core utilities. The component uses the\n            library's processors to handle flexible input types - notice how the\n            host can be passed as a simple string, but gets properly converted\n            to a Person schema type.\n          </p>\n        </section>\n\n        <section style={{ marginTop: \"2rem\" }}>\n          <h2>Recent Episodes</h2>\n          <ul>\n            <li>Episode 1: AI Revolution (30 min)</li>\n            <li>Episode 2: Web3 Explained (45 min)</li>\n            <li>Episode 3: Quantum Computing (35 min)</li>\n          </ul>\n        </section>\n\n        <section\n          style={{\n            marginTop: \"2rem\",\n            padding: \"1rem\",\n            backgroundColor: \"#f5f5f5\",\n            borderRadius: \"8px\",\n          }}\n        >\n          <h3>Developer Note</h3>\n          <p style={{ fontSize: \"0.9rem\" }}>\n            This custom component was created using:\n          </p>\n          <ul style={{ fontSize: \"0.9rem\" }}>\n            <li>JsonLdScript from next-seo for rendering</li>\n            <li>processors.processAuthor() for flexible host input</li>\n            <li>processors.processImage() for image handling</li>\n            <li>\n              The @type optional pattern - no manual type specification needed!\n            </li>\n          </ul>\n        </section>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/custom-service/page.tsx",
    "content": "import { ServiceJsonLd } from \"../../components/custom/ServiceJsonLd\";\n\nexport default function CustomServicePage() {\n  return (\n    <div className=\"container\">\n      <ServiceJsonLd\n        name=\"Web Development Services\"\n        serviceType=\"Professional Service\"\n        description=\"Full-stack web development and consulting services for modern businesses\"\n        provider={{\n          name: \"Tech Solutions Inc\",\n          url: \"https://example.com\",\n          logo: \"https://example.com/logo.png\",\n          address: {\n            streetAddress: \"123 Tech Street\",\n            addressLocality: \"San Francisco\",\n            addressRegion: \"CA\",\n            postalCode: \"94105\",\n            addressCountry: \"US\",\n          },\n        }}\n        areaServed={[\"US\", \"CA\", \"UK\", \"AU\"]}\n        url=\"https://example.com/services/web-development\"\n        offers={{\n          priceRange: \"$$$\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.8,\n          reviewCount: 127,\n          bestRating: 5,\n          worstRating: 1,\n        }}\n      />\n\n      <main style={{ padding: \"2rem\", maxWidth: \"800px\", margin: \"0 auto\" }}>\n        <h1>Web Development Services</h1>\n        <p style={{ color: \"#666\", marginBottom: \"2rem\" }}>\n          Custom Component Demo - Service JSON-LD with processors\n        </p>\n\n        <section>\n          <h2>Our Services</h2>\n          <p>\n            This page demonstrates a custom Service JSON-LD component built\n            using next-seo's core utilities. Notice how the provider can be\n            passed as either a string or a complex object, and it gets properly\n            converted to an Organization schema type.\n          </p>\n          <ul>\n            <li>Frontend Development (React, Next.js, Vue)</li>\n            <li>Backend Development (Node.js, Python, Go)</li>\n            <li>API Design and Implementation</li>\n            <li>Database Architecture</li>\n            <li>Cloud Infrastructure Setup</li>\n          </ul>\n        </section>\n\n        <section style={{ marginTop: \"2rem\" }}>\n          <h2>Service Areas</h2>\n          <p>We proudly serve clients in:</p>\n          <ul>\n            <li>United States</li>\n            <li>Canada</li>\n            <li>United Kingdom</li>\n            <li>Australia</li>\n          </ul>\n        </section>\n\n        <section style={{ marginTop: \"2rem\" }}>\n          <h2>Client Reviews</h2>\n          <div\n            style={{\n              padding: \"1rem\",\n              backgroundColor: \"#f0f8ff\",\n              borderRadius: \"8px\",\n            }}\n          >\n            <strong>⭐ 4.8/5.0</strong> based on 127 reviews\n          </div>\n        </section>\n\n        <section\n          style={{\n            marginTop: \"2rem\",\n            padding: \"1rem\",\n            backgroundColor: \"#f5f5f5\",\n            borderRadius: \"8px\",\n          }}\n        >\n          <h3>Developer Note</h3>\n          <p style={{ fontSize: \"0.9rem\" }}>\n            This custom component demonstrates:\n          </p>\n          <ul style={{ fontSize: \"0.9rem\" }}>\n            <li>\n              processors.processOrganization() for flexible provider input\n            </li>\n            <li>processors.processAggregateRating() for rating data</li>\n            <li>\n              Array handling for areaServed (converts single string to array)\n            </li>\n            <li>\n              Complex nested objects without requiring @type specifications\n            </li>\n          </ul>\n          <p style={{ fontSize: \"0.9rem\", marginTop: \"1rem\" }}>\n            The provider field accepts both a simple string (e.g., \"Tech\n            Solutions Inc\") or a complex object with address, logo, and other\n            properties. The processor automatically detects and applies the\n            correct @type.\n          </p>\n        </section>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/dataset/page.tsx",
    "content": "import { DatasetJsonLd } from \"next-seo\";\n\nexport default function DatasetPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <DatasetJsonLd\n        name=\"NCDC Storm Events Database\"\n        description=\"Storm Data is provided by the National Weather Service (NWS) and contain statistics on personal injuries and damage estimates.\"\n        url=\"https://example.com/dataset/storm-events\"\n        creator=\"NOAA\"\n        distribution={{\n          contentUrl: \"https://www.ncdc.noaa.gov/stormevents/ftp.jsp\",\n          encodingFormat: \"CSV\",\n        }}\n        keywords={[\"storm\", \"weather\", \"climate\", \"natural disasters\"]}\n        isAccessibleForFree={true}\n      />\n\n      <main className=\"max-w-4xl\">\n        <h1 className=\"text-3xl font-bold mb-4\">NCDC Storm Events Database</h1>\n\n        <div className=\"bg-gray-100 p-4 rounded mb-6\">\n          <p className=\"text-sm text-gray-600 mb-2\">Dataset Information</p>\n          <p className=\"font-semibold\">Format: CSV</p>\n          <p className=\"font-semibold\">Access: Free</p>\n          <p className=\"font-semibold\">Provider: NOAA</p>\n        </div>\n\n        <section className=\"prose max-w-none\">\n          <h2>Description</h2>\n          <p>\n            Storm Data is provided by the National Weather Service (NWS) and\n            contain statistics on personal injuries and damage estimates.\n          </p>\n\n          <h2>Keywords</h2>\n          <div className=\"flex gap-2 mt-2\">\n            {[\"storm\", \"weather\", \"climate\", \"natural disasters\"].map(\n              (keyword) => (\n                <span\n                  key={keyword}\n                  className=\"px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm\"\n                >\n                  {keyword}\n                </span>\n              ),\n            )}\n          </div>\n\n          <h2>Download</h2>\n          <a\n            href=\"https://www.ncdc.noaa.gov/stormevents/ftp.jsp\"\n            className=\"inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700\"\n          >\n            Download CSV Dataset\n          </a>\n        </section>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/dataset-advanced/page.tsx",
    "content": "import { DatasetJsonLd } from \"next-seo\";\n\nexport default function DatasetAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <DatasetJsonLd\n        name=\"Global Climate Data 2020-2024\"\n        description=\"Comprehensive climate measurements including temperature, precipitation, and atmospheric data collected from weather stations worldwide. This dataset provides hourly observations from over 10,000 weather stations globally, enabling detailed climate analysis and research.\"\n        url=\"https://example.com/datasets/global-climate-2020-2024\"\n        sameAs={[\n          \"https://doi.org/10.1000/182\",\n          \"https://data.gov/dataset/climate-2020-2024\",\n        ]}\n        identifier={[\n          \"https://doi.org/10.1000/182\",\n          {\n            value: \"ark:/12345/fk1234\",\n            propertyID: \"ARK\",\n          },\n        ]}\n        keywords={[\n          \"climate\",\n          \"temperature\",\n          \"precipitation\",\n          \"weather\",\n          \"atmospheric data\",\n          \"global warming\",\n          \"climate change\",\n        ]}\n        license={{\n          name: \"Creative Commons Zero v1.0 Universal\",\n          url: \"https://creativecommons.org/publicdomain/zero/1.0/\",\n        }}\n        isAccessibleForFree={true}\n        creator={[\n          {\n            name: \"National Centers for Environmental Information\",\n            url: \"https://www.ncei.noaa.gov/\",\n            contactPoint: {\n              contactType: \"customer service\",\n              telephone: \"+1-828-271-4800\",\n              email: \"ncei.orders@noaa.gov\",\n            },\n          },\n          \"Dr. Jane Smith\",\n          \"Dr. John Doe\",\n        ]}\n        funder={{\n          name: \"National Science Foundation\",\n          sameAs: \"https://ror.org/021nxhr62\",\n        }}\n        includedInDataCatalog={{\n          name: \"data.gov\",\n          url: \"https://data.gov\",\n        }}\n        distribution={[\n          {\n            contentUrl: \"https://example.com/data/climate-2020-2024.csv\",\n            encodingFormat: \"CSV\",\n            contentSize: \"2.5GB\",\n            description: \"Complete dataset in CSV format with all measurements\",\n          },\n          {\n            contentUrl: \"https://example.com/data/climate-2020-2024.json\",\n            encodingFormat: \"JSON\",\n            contentSize: \"3.1GB\",\n            description:\n              \"Complete dataset in JSON format for programmatic access\",\n          },\n          {\n            contentUrl: \"https://example.com/data/climate-2020-2024.nc\",\n            encodingFormat: \"NetCDF\",\n            contentSize: \"1.8GB\",\n            description: \"NetCDF format optimized for scientific analysis\",\n          },\n        ]}\n        temporalCoverage=\"2020-01-01/2024-12-31\"\n        spatialCoverage={{\n          name: \"Global Coverage\",\n          geo: {\n            box: \"-90 -180 90 180\",\n          },\n        }}\n        measurementTechnique={[\n          \"Satellite observation\",\n          \"Ground station measurements\",\n          \"Weather balloon data\",\n          \"Ocean buoy sensors\",\n        ]}\n        variableMeasured={[\n          \"temperature\",\n          \"precipitation\",\n          {\n            name: \"Atmospheric Pressure\",\n            value: \"hectopascals\",\n            propertyID: \"PRES\",\n          },\n          {\n            name: \"Wind Speed\",\n            value: \"meters per second\",\n            propertyID: \"WSPD\",\n          },\n          {\n            name: \"Relative Humidity\",\n            value: \"percentage\",\n            propertyID: \"RHUM\",\n          },\n        ]}\n        version=\"2.1\"\n        alternateName={[\n          \"Global Climate Dataset 2020-2024\",\n          \"GCD-2024\",\n          \"World Climate Data Collection\",\n        ]}\n        citation={[\n          \"Smith, J. et al. (2024) Global Climate Patterns 2020-2024. Nature Climate Change. https://doi.org/10.1038/s41558-024-01234\",\n          {\n            name: \"Global Climate Data Technical Report\",\n            url: \"https://example.com/reports/climate-2024-technical\",\n            identifier: \"TR-2024-001\",\n          },\n        ]}\n      />\n\n      <main className=\"max-w-6xl\">\n        <h1 className=\"text-4xl font-bold mb-6\">\n          Global Climate Data 2020-2024\n        </h1>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4 mb-8\">\n          <div className=\"bg-blue-50 p-4 rounded-lg\">\n            <h3 className=\"font-semibold text-blue-900 mb-2\">Coverage</h3>\n            <p className=\"text-sm\">Global</p>\n            <p className=\"text-sm\">2020-2024</p>\n          </div>\n          <div className=\"bg-green-50 p-4 rounded-lg\">\n            <h3 className=\"font-semibold text-green-900 mb-2\">Access</h3>\n            <p className=\"text-sm\">Free & Open</p>\n            <p className=\"text-sm\">CC0 License</p>\n          </div>\n          <div className=\"bg-purple-50 p-4 rounded-lg\">\n            <h3 className=\"font-semibold text-purple-900 mb-2\">Version</h3>\n            <p className=\"text-sm\">2.1</p>\n            <p className=\"text-sm\">3 Formats</p>\n          </div>\n        </div>\n\n        <section className=\"prose max-w-none mb-8\">\n          <h2>About This Dataset</h2>\n          <p>\n            Comprehensive climate measurements including temperature,\n            precipitation, and atmospheric data collected from weather stations\n            worldwide. This dataset provides hourly observations from over\n            10,000 weather stations globally, enabling detailed climate analysis\n            and research.\n          </p>\n\n          <h2>Variables Measured</h2>\n          <ul>\n            <li>Temperature (Celsius)</li>\n            <li>Precipitation (mm)</li>\n            <li>Atmospheric Pressure (hectopascals)</li>\n            <li>Wind Speed (meters per second)</li>\n            <li>Relative Humidity (percentage)</li>\n          </ul>\n\n          <h2>Data Collection Methods</h2>\n          <ul>\n            <li>Satellite observation</li>\n            <li>Ground station measurements</li>\n            <li>Weather balloon data</li>\n            <li>Ocean buoy sensors</li>\n          </ul>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-bold mb-4\">Download Options</h2>\n          <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n            <div className=\"border rounded-lg p-4\">\n              <h3 className=\"font-semibold mb-2\">CSV Format</h3>\n              <p className=\"text-sm text-gray-600 mb-3\">\n                2.5GB - Complete dataset\n              </p>\n              <button className=\"w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">\n                Download CSV\n              </button>\n            </div>\n            <div className=\"border rounded-lg p-4\">\n              <h3 className=\"font-semibold mb-2\">JSON Format</h3>\n              <p className=\"text-sm text-gray-600 mb-3\">\n                3.1GB - Programmatic access\n              </p>\n              <button className=\"w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">\n                Download JSON\n              </button>\n            </div>\n            <div className=\"border rounded-lg p-4\">\n              <h3 className=\"font-semibold mb-2\">NetCDF Format</h3>\n              <p className=\"text-sm text-gray-600 mb-3\">\n                1.8GB - Scientific analysis\n              </p>\n              <button className=\"w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">\n                Download NetCDF\n              </button>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-bold mb-4\">Citation</h2>\n          <div className=\"bg-gray-100 p-4 rounded\">\n            <p className=\"font-mono text-sm\">\n              Smith, J. et al. (2024) Global Climate Patterns 2020-2024. Nature\n              Climate Change. https://doi.org/10.1038/s41558-024-01234\n            </p>\n          </div>\n        </section>\n\n        <section>\n          <h2 className=\"text-2xl font-bold mb-4\">Contact</h2>\n          <p>\n            National Centers for Environmental Information\n            <br />\n            Email: ncei.orders@noaa.gov\n            <br />\n            Phone: +1-828-271-4800\n          </p>\n        </section>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/dataset-catalog/page.tsx",
    "content": "import { DatasetJsonLd } from \"next-seo\";\n\nexport default function DatasetCatalogPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <DatasetJsonLd\n        name=\"Ocean Temperature Time Series 2023\"\n        description=\"Daily ocean temperature measurements from Pacific Ocean monitoring stations. Part of the larger Pacific Ocean Climate Monitoring Program dataset collection.\"\n        url=\"https://example.com/datasets/ocean-temp-2023\"\n        identifier=\"https://doi.org/10.5000/ocean-temp-2023\"\n        keywords={[\"ocean\", \"temperature\", \"Pacific\", \"climate\", \"time series\"]}\n        license=\"https://creativecommons.org/licenses/by/4.0/\"\n        isAccessibleForFree={true}\n        creator={{\n          name: \"Pacific Ocean Research Institute\",\n          url: \"https://example.com/pori\",\n          logo: \"https://example.com/pori-logo.png\",\n        }}\n        includedInDataCatalog={{\n          name: \"Pacific Ocean Climate Data Catalog\",\n          url: \"https://example.com/pacific-climate-catalog\",\n          description:\n            \"Comprehensive collection of Pacific Ocean climate datasets\",\n        }}\n        distribution={{\n          contentUrl: \"https://example.com/data/ocean-temp-2023.csv\",\n          encodingFormat: \"CSV\",\n          contentSize: \"156MB\",\n        }}\n        temporalCoverage=\"2023-01-01/2023-12-31\"\n        spatialCoverage={{\n          name: \"Pacific Ocean\",\n          geo: {\n            box: \"-60 120 60 -80\",\n          },\n        }}\n        variableMeasured={[\n          {\n            name: \"Sea Surface Temperature\",\n            value: \"degrees Celsius\",\n            propertyID: \"SST\",\n          },\n          {\n            name: \"Temperature at 10m depth\",\n            value: \"degrees Celsius\",\n            propertyID: \"T10\",\n          },\n        ]}\n      />\n\n      <main className=\"max-w-4xl\">\n        <div className=\"bg-blue-50 border-l-4 border-blue-400 p-4 mb-6\">\n          <p className=\"text-sm text-blue-800\">\n            Part of the <strong>Pacific Ocean Climate Data Catalog</strong>\n          </p>\n        </div>\n\n        <h1 className=\"text-3xl font-bold mb-4\">\n          Ocean Temperature Time Series 2023\n        </h1>\n\n        <section className=\"prose max-w-none mb-8\">\n          <h2>Dataset Overview</h2>\n          <p>\n            Daily ocean temperature measurements from Pacific Ocean monitoring\n            stations. Part of the larger Pacific Ocean Climate Monitoring\n            Program dataset collection.\n          </p>\n\n          <div className=\"bg-gray-100 p-4 rounded my-4\">\n            <h3 className=\"text-lg font-semibold mb-2\">Quick Facts</h3>\n            <ul className=\"list-none space-y-1\">\n              <li>\n                📅 <strong>Period:</strong> January 1 - December 31, 2023\n              </li>\n              <li>\n                📍 <strong>Coverage:</strong> Pacific Ocean\n              </li>\n              <li>\n                📊 <strong>Size:</strong> 156MB\n              </li>\n              <li>\n                📄 <strong>Format:</strong> CSV\n              </li>\n              <li>\n                🔓 <strong>License:</strong> CC BY 4.0\n              </li>\n            </ul>\n          </div>\n\n          <h2>Variables Included</h2>\n          <table className=\"w-full border-collapse\">\n            <thead>\n              <tr className=\"bg-gray-200\">\n                <th className=\"border p-2 text-left\">Variable</th>\n                <th className=\"border p-2 text-left\">Unit</th>\n                <th className=\"border p-2 text-left\">Code</th>\n              </tr>\n            </thead>\n            <tbody>\n              <tr>\n                <td className=\"border p-2\">Sea Surface Temperature</td>\n                <td className=\"border p-2\">degrees Celsius</td>\n                <td className=\"border p-2 font-mono\">SST</td>\n              </tr>\n              <tr>\n                <td className=\"border p-2\">Temperature at 10m depth</td>\n                <td className=\"border p-2\">degrees Celsius</td>\n                <td className=\"border p-2 font-mono\">T10</td>\n              </tr>\n            </tbody>\n          </table>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-bold mb-4\">Data Catalog</h2>\n          <div className=\"border rounded-lg p-6 bg-gradient-to-r from-blue-50 to-cyan-50\">\n            <h3 className=\"text-xl font-semibold mb-2\">\n              Pacific Ocean Climate Data Catalog\n            </h3>\n            <p className=\"text-gray-700 mb-4\">\n              This dataset is part of a comprehensive collection of Pacific\n              Ocean climate datasets maintained by the Pacific Ocean Research\n              Institute.\n            </p>\n            <a\n              href=\"https://example.com/pacific-climate-catalog\"\n              className=\"text-blue-600 hover:underline\"\n            >\n              Browse all datasets in this catalog →\n            </a>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-bold mb-4\">Download</h2>\n          <div className=\"flex gap-4\">\n            <a\n              href=\"https://example.com/data/ocean-temp-2023.csv\"\n              className=\"inline-block px-6 py-3 bg-blue-600 text-white rounded hover:bg-blue-700\"\n            >\n              Download Dataset (CSV, 156MB)\n            </a>\n            <a\n              href=\"https://doi.org/10.5000/ocean-temp-2023\"\n              className=\"inline-block px-6 py-3 border border-gray-300 rounded hover:bg-gray-50\"\n            >\n              View DOI Record\n            </a>\n          </div>\n        </section>\n\n        <footer className=\"text-sm text-gray-600 border-t pt-4\">\n          <p>\n            Dataset provided by the Pacific Ocean Research Institute. Licensed\n            under Creative Commons Attribution 4.0 International.\n          </p>\n        </footer>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/dataset-nested/page.tsx",
    "content": "import { DatasetJsonLd } from \"next-seo\";\n\nexport default function DatasetNestedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <DatasetJsonLd\n        name=\"World Climate Database 2020-2024\"\n        description=\"Comprehensive global climate database containing regional climate datasets from all continents. This parent dataset aggregates climate measurements from North America, Europe, Asia, and other regions.\"\n        url=\"https://example.com/datasets/world-climate-database\"\n        identifier=\"https://doi.org/10.1000/world-climate-2024\"\n        keywords={[\n          \"climate\",\n          \"global\",\n          \"weather\",\n          \"temperature\",\n          \"aggregate dataset\",\n        ]}\n        license=\"https://creativecommons.org/publicdomain/zero/1.0/\"\n        isAccessibleForFree={true}\n        creator={{\n          name: \"Global Climate Research Consortium\",\n          url: \"https://example.com/gcrc\",\n        }}\n        hasPart={[\n          {\n            \"@type\": \"Dataset\",\n            name: \"North America Climate Data 2020-2024\",\n            description:\n              \"Climate measurements from weather stations across North America including temperature, precipitation, and wind data.\",\n            url: \"https://example.com/datasets/na-climate-2024\",\n            license: \"https://creativecommons.org/publicdomain/zero/1.0/\",\n            creator: {\n              \"@type\": \"Organization\",\n              name: \"North American Weather Service\",\n            },\n            distribution: {\n              \"@type\": \"DataDownload\",\n              contentUrl: \"https://example.com/data/na-climate.csv\",\n              encodingFormat: \"CSV\",\n            },\n            spatialCoverage: {\n              \"@type\": \"Place\",\n              name: \"North America\",\n            },\n          },\n          {\n            \"@type\": \"Dataset\",\n            name: \"Europe Climate Data 2020-2024\",\n            description:\n              \"Comprehensive climate data from European monitoring stations with hourly measurements.\",\n            url: \"https://example.com/datasets/eu-climate-2024\",\n            license: \"https://creativecommons.org/publicdomain/zero/1.0/\",\n            creator: {\n              \"@type\": \"Organization\",\n              name: \"European Climate Agency\",\n            },\n            distribution: {\n              \"@type\": \"DataDownload\",\n              contentUrl: \"https://example.com/data/eu-climate.csv\",\n              encodingFormat: \"CSV\",\n            },\n            spatialCoverage: {\n              \"@type\": \"Place\",\n              name: \"Europe\",\n            },\n          },\n          {\n            \"@type\": \"Dataset\",\n            name: \"Asia Pacific Climate Data 2020-2024\",\n            description:\n              \"Climate observations from the Asia Pacific region including monsoon patterns and tropical systems.\",\n            url: \"https://example.com/datasets/apac-climate-2024\",\n            license: \"https://creativecommons.org/publicdomain/zero/1.0/\",\n            creator: {\n              \"@type\": \"Organization\",\n              name: \"Asia Pacific Climate Center\",\n            },\n            distribution: {\n              \"@type\": \"DataDownload\",\n              contentUrl: \"https://example.com/data/apac-climate.csv\",\n              encodingFormat: \"CSV\",\n            },\n            spatialCoverage: {\n              \"@type\": \"Place\",\n              name: \"Asia Pacific\",\n            },\n          },\n        ]}\n        distribution={{\n          contentUrl: \"https://example.com/data/world-climate-complete.zip\",\n          encodingFormat: \"ZIP\",\n          contentSize: \"12.5GB\",\n          description: \"Complete aggregated dataset with all regional data\",\n        }}\n        temporalCoverage=\"2020-01-01/2024-12-31\"\n        spatialCoverage=\"Global\"\n        variableMeasured={[\n          \"temperature\",\n          \"precipitation\",\n          \"wind speed\",\n          \"humidity\",\n        ]}\n      />\n\n      <main className=\"max-w-5xl\">\n        <h1 className=\"text-4xl font-bold mb-6\">\n          World Climate Database 2020-2024\n        </h1>\n\n        <div className=\"bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6\">\n          <p className=\"text-sm\">\n            <strong>Aggregate Dataset:</strong> This parent dataset contains 3\n            regional sub-datasets\n          </p>\n        </div>\n\n        <section className=\"prose max-w-none mb-8\">\n          <h2>Overview</h2>\n          <p>\n            Comprehensive global climate database containing regional climate\n            datasets from all continents. This parent dataset aggregates climate\n            measurements from North America, Europe, Asia, and other regions.\n          </p>\n\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 my-6\">\n            <div className=\"bg-gray-100 p-4 rounded\">\n              <h3 className=\"font-semibold mb-2\">Dataset Structure</h3>\n              <ul className=\"list-disc list-inside space-y-1\">\n                <li>Parent dataset (aggregated)</li>\n                <li>3 regional sub-datasets</li>\n                <li>Unified data schema</li>\n                <li>Consistent temporal coverage</li>\n              </ul>\n            </div>\n            <div className=\"bg-gray-100 p-4 rounded\">\n              <h3 className=\"font-semibold mb-2\">Key Features</h3>\n              <ul className=\"list-disc list-inside space-y-1\">\n                <li>Global coverage</li>\n                <li>5-year time span</li>\n                <li>Hourly measurements</li>\n                <li>Quality controlled</li>\n              </ul>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-bold mb-4\">Component Datasets</h2>\n          <div className=\"space-y-4\">\n            <div className=\"border rounded-lg p-6 hover:shadow-lg transition-shadow\">\n              <h3 className=\"text-xl font-semibold mb-2\">\n                🌎 North America Climate Data 2020-2024\n              </h3>\n              <p className=\"text-gray-700 mb-3\">\n                Climate measurements from weather stations across North America\n                including temperature, precipitation, and wind data.\n              </p>\n              <div className=\"flex gap-4 text-sm\">\n                <span className=\"text-gray-600\">\n                  Provider: North American Weather Service\n                </span>\n                <span className=\"text-gray-600\">Format: CSV</span>\n              </div>\n              <a\n                href=\"https://example.com/datasets/na-climate-2024\"\n                className=\"inline-block mt-3 text-blue-600 hover:underline\"\n              >\n                View dataset →\n              </a>\n            </div>\n\n            <div className=\"border rounded-lg p-6 hover:shadow-lg transition-shadow\">\n              <h3 className=\"text-xl font-semibold mb-2\">\n                🌍 Europe Climate Data 2020-2024\n              </h3>\n              <p className=\"text-gray-700 mb-3\">\n                Comprehensive climate data from European monitoring stations\n                with hourly measurements.\n              </p>\n              <div className=\"flex gap-4 text-sm\">\n                <span className=\"text-gray-600\">\n                  Provider: European Climate Agency\n                </span>\n                <span className=\"text-gray-600\">Format: CSV</span>\n              </div>\n              <a\n                href=\"https://example.com/datasets/eu-climate-2024\"\n                className=\"inline-block mt-3 text-blue-600 hover:underline\"\n              >\n                View dataset →\n              </a>\n            </div>\n\n            <div className=\"border rounded-lg p-6 hover:shadow-lg transition-shadow\">\n              <h3 className=\"text-xl font-semibold mb-2\">\n                🌏 Asia Pacific Climate Data 2020-2024\n              </h3>\n              <p className=\"text-gray-700 mb-3\">\n                Climate observations from the Asia Pacific region including\n                monsoon patterns and tropical systems.\n              </p>\n              <div className=\"flex gap-4 text-sm\">\n                <span className=\"text-gray-600\">\n                  Provider: Asia Pacific Climate Center\n                </span>\n                <span className=\"text-gray-600\">Format: CSV</span>\n              </div>\n              <a\n                href=\"https://example.com/datasets/apac-climate-2024\"\n                className=\"inline-block mt-3 text-blue-600 hover:underline\"\n              >\n                View dataset →\n              </a>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-bold mb-4\">Download Complete Dataset</h2>\n          <div className=\"bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-lg\">\n            <p className=\"mb-4\">\n              Download the complete aggregated dataset containing all regional\n              data in a single archive.\n            </p>\n            <div className=\"flex items-center gap-6\">\n              <a\n                href=\"https://example.com/data/world-climate-complete.zip\"\n                className=\"inline-block px-6 py-3 bg-blue-600 text-white rounded hover:bg-blue-700\"\n              >\n                Download Complete Dataset (ZIP, 12.5GB)\n              </a>\n              <span className=\"text-sm text-gray-600\">\n                Contains all 3 regional datasets\n              </span>\n            </div>\n          </div>\n        </section>\n\n        <section>\n          <h2 className=\"text-2xl font-bold mb-4\">Data Hierarchy</h2>\n          <div className=\"bg-gray-100 p-6 rounded\">\n            <pre className=\"text-sm\">\n              {`World Climate Database 2020-2024 (Parent)\n├── North America Climate Data 2020-2024\n├── Europe Climate Data 2020-2024\n└── Asia Pacific Climate Data 2020-2024`}\n            </pre>\n          </div>\n        </section>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/discussion-forum/page.tsx",
    "content": "import { DiscussionForumPostingJsonLd } from \"next-seo\";\n\nexport default function DiscussionForumPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <DiscussionForumPostingJsonLd\n        headline=\"I went to the concert!\"\n        text=\"Look at how cool this concert was!\"\n        author=\"Katie Pope\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        url=\"https://example.com/forum/very-popular-thread\"\n        comment={[\n          {\n            text: \"Who's the person you're with?\",\n            author: \"Saul Douglas\",\n            datePublished: \"2024-01-01T09:46:02+00:00\",\n          },\n          {\n            text: \"That's my mom, isn't she cool?\",\n            author: \"Katie Pope\",\n            datePublished: \"2024-01-01T09:50:25+00:00\",\n          },\n        ]}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>I went to the concert!</h1>\n        <div className=\"text-sm text-gray-600 mb-4\">\n          Posted by Katie Pope on January 1, 2024\n        </div>\n        <p>Look at how cool this concert was!</p>\n\n        <div className=\"mt-8 space-y-4\">\n          <h2>Comments</h2>\n          <div className=\"border-l-4 border-gray-200 pl-4\">\n            <p className=\"font-semibold\">Saul Douglas</p>\n            <p>Who's the person you're with?</p>\n          </div>\n          <div className=\"border-l-4 border-gray-200 pl-4\">\n            <p className=\"font-semibold\">Katie Pope</p>\n            <p>That's my mom, isn't she cool?</p>\n          </div>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/discussion-forum-advanced/page.tsx",
    "content": "import { DiscussionForumPostingJsonLd } from \"next-seo\";\n\nexport default function DiscussionForumAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <DiscussionForumPostingJsonLd\n        headline=\"Very Popular Thread About Concerts\"\n        text=\"I went to an amazing concert last night! The atmosphere was electric and the band played all their hits.\"\n        image={[\n          \"https://example.com/concert-photo1.jpg\",\n          {\n            url: \"https://example.com/concert-photo2.jpg\",\n            width: 1200,\n            height: 800,\n            caption: \"The main stage\",\n          },\n        ]}\n        video={{\n          name: \"Concert Highlights\",\n          contentUrl: \"https://example.com/concert-video.mp4\",\n          uploadDate: \"2024-01-02T10:00:00+00:00\",\n          thumbnailUrl: \"https://example.com/concert-thumbnail.jpg\",\n          description: \"Best moments from the concert\",\n        }}\n        url=\"https://example.com/forum/concerts/very-popular-thread\"\n        author={{\n          name: \"Katie Pope\",\n          url: \"https://example.com/user/katie-pope\",\n        }}\n        datePublished=\"2024-01-01T08:34:34+00:00\"\n        dateModified=\"2024-01-01T09:00:00+00:00\"\n        interactionStatistic={[\n          {\n            interactionType: \"https://schema.org/LikeAction\",\n            userInteractionCount: 127,\n          },\n          {\n            interactionType: \"https://schema.org/ViewAction\",\n            userInteractionCount: 3420,\n          },\n          {\n            interactionType: \"https://schema.org/CommentAction\",\n            userInteractionCount: 23,\n          },\n        ]}\n        isPartOf={{\n          name: \"Concert Discussions\",\n          url: \"https://example.com/forum/concerts\",\n        }}\n        sharedContent={{\n          url: \"https://example.com/concert-tickets\",\n          name: \"Concert Venue Information\",\n          description: \"Details about the venue and upcoming shows\",\n        }}\n        comment={[\n          {\n            text: \"This should not be this popular\",\n            author: {\n              name: \"Forum Critic\",\n              url: \"https://example.com/user/forum-critic\",\n            },\n            datePublished: \"2024-01-01T09:00:00+00:00\",\n            interactionStatistic: {\n              interactionType: \"https://schema.org/DislikeAction\",\n              userInteractionCount: 5,\n            },\n            comment: [\n              {\n                text: \"Yes it should, it's a great post!\",\n                author: \"Happy Fan\",\n                datePublished: \"2024-01-01T09:30:00+00:00\",\n                interactionStatistic: {\n                  interactionType: \"https://schema.org/LikeAction\",\n                  userInteractionCount: 15,\n                },\n              },\n            ],\n          },\n          {\n            text: \"I was at the same concert! Here's my video:\",\n            author: {\n              name: \"Concert Goer\",\n              url: \"https://example.com/user/concert-goer\",\n            },\n            datePublished: \"2024-01-01T10:00:00+00:00\",\n            video: {\n              name: \"My Concert Video\",\n              contentUrl: \"https://example.com/user-concert-video.mp4\",\n              uploadDate: \"2024-01-01T11:00:00+00:00\",\n              thumbnailUrl: \"https://example.com/user-video-thumb.jpg\",\n              description: \"My perspective from the crowd\",\n            },\n          },\n        ]}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Very Popular Thread About Concerts</h1>\n        <div className=\"text-sm text-gray-600 mb-4\">\n          Posted by <a href=\"https://example.com/user/katie-pope\">Katie Pope</a>{\" \"}\n          on January 1, 2024\n          <br />\n          127 likes · 3,420 views · 23 comments\n        </div>\n\n        <p>\n          I went to an amazing concert last night! The atmosphere was electric\n          and the band played all their hits.\n        </p>\n\n        <div className=\"my-4 bg-gray-100 p-4 rounded\">\n          <h3>Shared Link:</h3>\n          <a href=\"https://example.com/concert-tickets\">\n            Concert Venue Information\n          </a>\n          <p className=\"text-sm\">Details about the venue and upcoming shows</p>\n        </div>\n\n        <div className=\"mt-8 space-y-4\">\n          <h2>Comments</h2>\n\n          <div className=\"border-l-4 border-gray-200 pl-4\">\n            <p className=\"font-semibold\">\n              <a href=\"https://example.com/user/forum-critic\">Forum Critic</a>\n              <span className=\"text-sm text-gray-500 ml-2\">5 dislikes</span>\n            </p>\n            <p>This should not be this popular</p>\n\n            <div className=\"ml-8 mt-2 border-l-4 border-gray-100 pl-4\">\n              <p className=\"font-semibold\">\n                Happy Fan\n                <span className=\"text-sm text-gray-500 ml-2\">15 likes</span>\n              </p>\n              <p>Yes it should, it's a great post!</p>\n            </div>\n          </div>\n\n          <div className=\"border-l-4 border-gray-200 pl-4\">\n            <p className=\"font-semibold\">\n              <a href=\"https://example.com/user/concert-goer\">Concert Goer</a>\n            </p>\n            <p>I was at the same concert! Here's my video:</p>\n            <div className=\"mt-2\">\n              <video controls className=\"w-full max-w-md\">\n                <source\n                  src=\"https://example.com/user-concert-video.mp4\"\n                  type=\"video/mp4\"\n                />\n              </video>\n            </div>\n          </div>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/discussion-forum-deleted/page.tsx",
    "content": "import { DiscussionForumPostingJsonLd } from \"next-seo\";\n\nexport default function DiscussionForumDeletedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <DiscussionForumPostingJsonLd\n        headline=\"[Deleted Post]\"\n        text=\"This post has been removed by the author or moderators.\"\n        author=\"DeletedUser\"\n        datePublished=\"2024-01-10T10:00:00+00:00\"\n        dateModified=\"2024-01-10T15:00:00+00:00\"\n        url=\"https://example.com/forum/deleted-thread-12345\"\n        creativeWorkStatus=\"Deleted\"\n        comment={[\n          {\n            text: \"Why was this deleted?\",\n            author: \"CuriousUser\",\n            datePublished: \"2024-01-10T16:00:00+00:00\",\n          },\n          {\n            text: \"[This comment has been deleted]\",\n            author: \"AnotherDeletedUser\",\n            datePublished: \"2024-01-10T16:30:00+00:00\",\n            creativeWorkStatus: \"Deleted\",\n          },\n          {\n            text: \"The original post violated community guidelines.\",\n            author: {\n              \"@type\": \"Organization\",\n              name: \"Forum Moderators\",\n              url: \"https://example.com/moderators\",\n            },\n            datePublished: \"2024-01-10T17:00:00+00:00\",\n          },\n        ]}\n        isPartOf={{\n          name: \"General Discussion\",\n          url: \"https://example.com/forum/general\",\n        }}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <div className=\"bg-gray-100 border border-gray-300 rounded-lg p-6 text-center\">\n          <h1 className=\"text-gray-600\">[Deleted Post]</h1>\n          <div className=\"text-sm text-gray-500 mb-4\">\n            Posted by DeletedUser on January 10, 2024\n            <br />\n            <span className=\"text-red-600\">\n              Deleted on January 10, 2024 at 3:00 PM\n            </span>\n          </div>\n          <p className=\"text-gray-600 italic\">\n            This post has been removed by the author or moderators.\n          </p>\n        </div>\n\n        <div className=\"mt-8 space-y-4\">\n          <h2>Comments</h2>\n          <p className=\"text-sm text-gray-600\">\n            Comments are preserved for context even though the original post has\n            been deleted.\n          </p>\n\n          <div className=\"border-l-4 border-gray-200 pl-4\">\n            <p className=\"font-semibold\">CuriousUser</p>\n            <p>Why was this deleted?</p>\n            <p className=\"text-xs text-gray-500\">January 10, 2024 at 4:00 PM</p>\n          </div>\n\n          <div className=\"border-l-4 border-gray-200 pl-4 bg-gray-50 p-3 rounded\">\n            <p className=\"font-semibold text-gray-600\">AnotherDeletedUser</p>\n            <p className=\"text-gray-500 italic\">\n              [This comment has been deleted]\n            </p>\n            <p className=\"text-xs text-gray-500\">January 10, 2024 at 4:30 PM</p>\n          </div>\n\n          <div className=\"border-l-4 border-blue-400 pl-4 bg-blue-50 p-3 rounded\">\n            <p className=\"font-semibold\">\n              <a\n                href=\"https://example.com/moderators\"\n                className=\"text-blue-600\"\n              >\n                Forum Moderators\n              </a>\n              <span className=\"ml-2 text-xs bg-blue-200 text-blue-800 px-2 py-1 rounded\">\n                Official\n              </span>\n            </p>\n            <p>The original post violated community guidelines.</p>\n            <p className=\"text-xs text-gray-500\">January 10, 2024 at 5:00 PM</p>\n          </div>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/employer-aggregate-rating/page.tsx",
    "content": "import { EmployerAggregateRatingJsonLd } from \"next-seo\";\n\nexport default function EmployerAggregateRatingPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <EmployerAggregateRatingJsonLd\n        itemReviewed=\"World's Best Coffee Shop\"\n        ratingValue={91}\n        ratingCount={10561}\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-6\">\n          World's Best Coffee Shop - Employer Ratings\n        </h1>\n\n        <section className=\"mb-8\">\n          <div className=\"bg-blue-50 p-6 rounded-lg mb-6\">\n            <h2 className=\"text-2xl font-semibold mb-4\">Overall Rating</h2>\n            <div className=\"flex items-center gap-4\">\n              <div className=\"text-5xl font-bold text-blue-600\">91</div>\n              <div>\n                <p className=\"text-gray-600\">out of 100</p>\n                <p className=\"text-sm text-gray-500\">\n                  Based on 10,561 employee ratings\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <h2 className=\"text-2xl font-semibold mb-4\">About This Company</h2>\n          <p className=\"text-gray-700 mb-4\">\n            World's Best Coffee Shop is known for its exceptional work culture\n            and employee satisfaction. Our baristas and staff consistently rate\n            us as one of the top employers in the food service industry.\n          </p>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">\n            What Employees Are Saying\n          </h2>\n          <div className=\"space-y-4\">\n            <div className=\"border-l-4 border-blue-500 pl-4\">\n              <p className=\"italic text-gray-700\">\n                \"Amazing workplace culture with great benefits and growth\n                opportunities!\"\n              </p>\n              <p className=\"text-sm text-gray-500 mt-2\">- Current Employee</p>\n            </div>\n            <div className=\"border-l-4 border-blue-500 pl-4\">\n              <p className=\"italic text-gray-700\">\n                \"Management truly cares about work-life balance and employee\n                wellbeing.\"\n              </p>\n              <p className=\"text-sm text-gray-500 mt-2\">- Former Employee</p>\n            </div>\n          </div>\n        </section>\n\n        <section>\n          <h2 className=\"text-2xl font-semibold mb-4\">Join Our Growing Team</h2>\n          <p className=\"text-gray-700 mb-4\">\n            We're always looking for passionate individuals to join our team.\n            Check out our current openings and become part of the World's Best\n            Coffee Shop family!\n          </p>\n          <button className=\"bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors\">\n            View Open Positions\n          </button>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/employer-aggregate-rating-advanced/page.tsx",
    "content": "import { EmployerAggregateRatingJsonLd } from \"next-seo\";\n\nexport default function EmployerAggregateRatingAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <EmployerAggregateRatingJsonLd\n        itemReviewed={{\n          name: \"TechCorp International\",\n          sameAs: \"https://www.techcorp-international.example.com\",\n          url: \"https://www.techcorp-international.example.com\",\n          logo: {\n            url: \"https://example.com/techcorp-logo.png\",\n            width: 600,\n            height: 300,\n          },\n          description:\n            \"Leading technology company specializing in cloud solutions and AI\",\n          telephone: \"+1-555-123-4567\",\n          email: \"careers@techcorp.example.com\",\n          address: [\n            {\n              streetAddress: \"123 Innovation Way\",\n              addressLocality: \"San Francisco\",\n              addressRegion: \"CA\",\n              postalCode: \"94105\",\n              addressCountry: \"US\",\n            },\n            {\n              streetAddress: \"456 Tech Park\",\n              addressLocality: \"New York\",\n              addressRegion: \"NY\",\n              postalCode: \"10001\",\n              addressCountry: \"US\",\n            },\n          ],\n          numberOfEmployees: {\n            value: 5000,\n          },\n        }}\n        ratingValue={4.7}\n        ratingCount={1842}\n        reviewCount={1755}\n        bestRating={5}\n        worstRating={1}\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <header className=\"mb-8\">\n          <div className=\"flex items-center gap-6 mb-6\">\n            <div>\n              <h1 className=\"text-4xl font-bold\">TechCorp International</h1>\n              <p className=\"text-xl text-gray-600\">\n                Leading technology company specializing in cloud solutions and\n                AI\n              </p>\n            </div>\n          </div>\n        </header>\n\n        <section className=\"mb-8\">\n          <div className=\"bg-gradient-to-r from-blue-50 to-purple-50 p-8 rounded-xl mb-6\">\n            <h2 className=\"text-2xl font-semibold mb-4\">Employee Ratings</h2>\n            <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n              <div className=\"text-center\">\n                <div className=\"text-5xl font-bold text-blue-600\">4.7</div>\n                <div className=\"text-gray-600\">out of 5.0</div>\n                <div className=\"text-sm text-gray-500 mt-2\">Overall Rating</div>\n              </div>\n              <div className=\"text-center\">\n                <div className=\"text-4xl font-bold text-purple-600\">1,842</div>\n                <div className=\"text-sm text-gray-500 mt-2\">Total Ratings</div>\n              </div>\n              <div className=\"text-center\">\n                <div className=\"text-4xl font-bold text-indigo-600\">1,755</div>\n                <div className=\"text-sm text-gray-500 mt-2\">\n                  Written Reviews\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <h2 className=\"text-2xl font-semibold mb-4\">Company Overview</h2>\n          <p className=\"text-gray-700 mb-4\">\n            TechCorp International is a global leader in cloud computing and\n            artificial intelligence solutions. With over 5,000 employees across\n            multiple offices, we're committed to creating an inclusive,\n            innovative workplace where everyone can thrive.\n          </p>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Our Locations</h2>\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n            <div className=\"border rounded-lg p-6\">\n              <h3 className=\"font-semibold mb-2\">San Francisco HQ</h3>\n              <p className=\"text-gray-600\">\n                123 Innovation Way\n                <br />\n                San Francisco, CA 94105\n              </p>\n            </div>\n            <div className=\"border rounded-lg p-6\">\n              <h3 className=\"font-semibold mb-2\">New York Office</h3>\n              <p className=\"text-gray-600\">\n                456 Tech Park\n                <br />\n                New York, NY 10001\n              </p>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">\n            Why Employees Love Working Here\n          </h2>\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n            <div className=\"flex items-start gap-3\">\n              <svg\n                className=\"w-6 h-6 text-green-500 flex-shrink-0\"\n                fill=\"currentColor\"\n                viewBox=\"0 0 20 20\"\n              >\n                <path\n                  fillRule=\"evenodd\"\n                  d=\"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z\"\n                  clipRule=\"evenodd\"\n                />\n              </svg>\n              <div>\n                <h3 className=\"font-semibold\">Competitive Benefits</h3>\n                <p className=\"text-gray-600 text-sm\">\n                  Comprehensive health coverage, 401k matching, and more\n                </p>\n              </div>\n            </div>\n            <div className=\"flex items-start gap-3\">\n              <svg\n                className=\"w-6 h-6 text-green-500 flex-shrink-0\"\n                fill=\"currentColor\"\n                viewBox=\"0 0 20 20\"\n              >\n                <path\n                  fillRule=\"evenodd\"\n                  d=\"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z\"\n                  clipRule=\"evenodd\"\n                />\n              </svg>\n              <div>\n                <h3 className=\"font-semibold\">Remote-First Culture</h3>\n                <p className=\"text-gray-600 text-sm\">\n                  Flexible work arrangements and global collaboration\n                </p>\n              </div>\n            </div>\n            <div className=\"flex items-start gap-3\">\n              <svg\n                className=\"w-6 h-6 text-green-500 flex-shrink-0\"\n                fill=\"currentColor\"\n                viewBox=\"0 0 20 20\"\n              >\n                <path\n                  fillRule=\"evenodd\"\n                  d=\"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z\"\n                  clipRule=\"evenodd\"\n                />\n              </svg>\n              <div>\n                <h3 className=\"font-semibold\">Career Growth</h3>\n                <p className=\"text-gray-600 text-sm\">\n                  Learning opportunities and clear advancement paths\n                </p>\n              </div>\n            </div>\n            <div className=\"flex items-start gap-3\">\n              <svg\n                className=\"w-6 h-6 text-green-500 flex-shrink-0\"\n                fill=\"currentColor\"\n                viewBox=\"0 0 20 20\"\n              >\n                <path\n                  fillRule=\"evenodd\"\n                  d=\"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z\"\n                  clipRule=\"evenodd\"\n                />\n              </svg>\n              <div>\n                <h3 className=\"font-semibold\">Innovation Focus</h3>\n                <p className=\"text-gray-600 text-sm\">\n                  Work on cutting-edge projects with latest technologies\n                </p>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        <section>\n          <h2 className=\"text-2xl font-semibold mb-4\">Contact Careers Team</h2>\n          <div className=\"bg-gray-50 p-6 rounded-lg\">\n            <p className=\"text-gray-700 mb-2\">\n              <strong>Email:</strong>{\" \"}\n              <a\n                href=\"mailto:careers@techcorp.example.com\"\n                className=\"text-blue-600 hover:underline\"\n              >\n                careers@techcorp.example.com\n              </a>\n            </p>\n            <p className=\"text-gray-700 mb-2\">\n              <strong>Phone:</strong> +1-555-123-4567\n            </p>\n            <p className=\"text-gray-700\">\n              <strong>Website:</strong>{\" \"}\n              <a\n                href=\"https://www.techcorp-international.example.com\"\n                className=\"text-blue-600 hover:underline\"\n              >\n                www.techcorp-international.example.com\n              </a>\n            </p>\n          </div>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/employer-aggregate-rating-custom-scale/page.tsx",
    "content": "import { EmployerAggregateRatingJsonLd } from \"next-seo\";\n\nexport default function EmployerAggregateRatingCustomScalePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <EmployerAggregateRatingJsonLd\n        itemReviewed={{\n          name: \"Green Energy Corp\",\n          sameAs: \"https://www.greenenergycorp.example.com\",\n        }}\n        ratingValue=\"85%\"\n        reviewCount={432}\n        bestRating={100}\n        worstRating={0}\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-6\">\n          Green Energy Corp - Employee Satisfaction Score\n        </h1>\n\n        <section className=\"mb-8\">\n          <div className=\"bg-green-50 p-8 rounded-xl mb-6\">\n            <h2 className=\"text-2xl font-semibold mb-4\">\n              Employee Satisfaction Index\n            </h2>\n            <div className=\"flex items-center gap-6\">\n              <div className=\"relative w-32 h-32\">\n                <svg className=\"w-full h-full transform -rotate-90\">\n                  <circle\n                    cx=\"64\"\n                    cy=\"64\"\n                    r=\"56\"\n                    stroke=\"#e5e7eb\"\n                    strokeWidth=\"16\"\n                    fill=\"none\"\n                  />\n                  <circle\n                    cx=\"64\"\n                    cy=\"64\"\n                    r=\"56\"\n                    stroke=\"#10b981\"\n                    strokeWidth=\"16\"\n                    fill=\"none\"\n                    strokeDasharray={`${2 * Math.PI * 56}`}\n                    strokeDashoffset={`${2 * Math.PI * 56 * (1 - 0.85)}`}\n                    className=\"transition-all duration-1000 ease-out\"\n                  />\n                </svg>\n                <div className=\"absolute inset-0 flex items-center justify-center\">\n                  <span className=\"text-3xl font-bold text-green-600\">85%</span>\n                </div>\n              </div>\n              <div>\n                <p className=\"text-gray-600\">\n                  Percentage-based satisfaction score\n                </p>\n                <p className=\"text-sm text-gray-500 mt-1\">\n                  Based on 432 employee reviews\n                </p>\n                <p className=\"text-xs text-gray-400 mt-2\">\n                  Scale: 0% (worst) to 100% (best)\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <h2 className=\"text-2xl font-semibold mb-4\">\n            About Our Rating System\n          </h2>\n          <p className=\"text-gray-700 mb-4\">\n            At Green Energy Corp, we use a percentage-based rating system where\n            employees rate their overall satisfaction from 0% to 100%. This\n            intuitive scale allows for more nuanced feedback compared to\n            traditional star ratings.\n          </p>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">\n            Rating Breakdown by Category\n          </h2>\n          <div className=\"space-y-4\">\n            <div>\n              <div className=\"flex justify-between mb-1\">\n                <span className=\"text-gray-700\">Work-Life Balance</span>\n                <span className=\"font-semibold\">88%</span>\n              </div>\n              <div className=\"w-full bg-gray-200 rounded-full h-3\">\n                <div\n                  className=\"bg-green-500 h-3 rounded-full\"\n                  style={{ width: \"88%\" }}\n                ></div>\n              </div>\n            </div>\n            <div>\n              <div className=\"flex justify-between mb-1\">\n                <span className=\"text-gray-700\">Compensation & Benefits</span>\n                <span className=\"font-semibold\">82%</span>\n              </div>\n              <div className=\"w-full bg-gray-200 rounded-full h-3\">\n                <div\n                  className=\"bg-green-500 h-3 rounded-full\"\n                  style={{ width: \"82%\" }}\n                ></div>\n              </div>\n            </div>\n            <div>\n              <div className=\"flex justify-between mb-1\">\n                <span className=\"text-gray-700\">Career Development</span>\n                <span className=\"font-semibold\">79%</span>\n              </div>\n              <div className=\"w-full bg-gray-200 rounded-full h-3\">\n                <div\n                  className=\"bg-green-500 h-3 rounded-full\"\n                  style={{ width: \"79%\" }}\n                ></div>\n              </div>\n            </div>\n            <div>\n              <div className=\"flex justify-between mb-1\">\n                <span className=\"text-gray-700\">Company Culture</span>\n                <span className=\"font-semibold\">91%</span>\n              </div>\n              <div className=\"w-full bg-gray-200 rounded-full h-3\">\n                <div\n                  className=\"bg-green-500 h-3 rounded-full\"\n                  style={{ width: \"91%\" }}\n                ></div>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">\n            What Makes Us Different\n          </h2>\n          <div className=\"bg-gradient-to-r from-green-100 to-blue-100 p-6 rounded-lg\">\n            <p className=\"text-gray-700 mb-4\">\n              Green Energy Corp is committed to sustainable practices not just\n              in our products, but also in how we treat our employees. Our\n              percentage-based rating system reflects our commitment to\n              transparency and continuous improvement.\n            </p>\n            <ul className=\"list-disc list-inside text-gray-700 space-y-2\">\n              <li>100% renewable energy powered offices</li>\n              <li>Flexible hybrid work arrangements</li>\n              <li>Comprehensive sustainability training</li>\n              <li>Employee equity participation program</li>\n            </ul>\n          </div>\n        </section>\n\n        <section>\n          <h2 className=\"text-2xl font-semibold mb-4\">Join Our Mission</h2>\n          <p className=\"text-gray-700 mb-4\">\n            Be part of a company that's making a real difference in the fight\n            against climate change while building a rewarding career.\n          </p>\n          <div className=\"flex gap-4\">\n            <button className=\"bg-green-600 text-white px-6 py-3 rounded-lg hover:bg-green-700 transition-colors\">\n              View Careers\n            </button>\n            <button className=\"border border-green-600 text-green-600 px-6 py-3 rounded-lg hover:bg-green-50 transition-colors\">\n              Learn More About Us\n            </button>\n          </div>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/event/page.tsx",
    "content": "import { EventJsonLd } from \"next-seo\";\n\nexport default function EventPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <EventJsonLd\n        name=\"The Adventures of Kira and Morrison\"\n        startDate=\"2025-07-21T19:00-05:00\"\n        endDate=\"2025-07-21T23:00-05:00\"\n        location={{\n          \"@type\": \"Place\",\n          name: \"Snickerpark Stadium\",\n          address: {\n            \"@type\": \"PostalAddress\",\n            streetAddress: \"100 West Snickerpark Dr\",\n            addressLocality: \"Snickertown\",\n            postalCode: \"19019\",\n            addressRegion: \"PA\",\n            addressCountry: \"US\",\n          },\n        }}\n        description=\"The Adventures of Kira and Morrison is coming to Snickertown in a can't miss performance.\"\n        image={[\n          \"https://example.com/photos/1x1/photo.jpg\",\n          \"https://example.com/photos/4x3/photo.jpg\",\n          \"https://example.com/photos/16x9/photo.jpg\",\n        ]}\n        offers={{\n          \"@type\": \"Offer\",\n          url: \"https://www.example.com/event_offer/12345_202403180430\",\n          price: 30,\n          priceCurrency: \"USD\",\n          availability: \"https://schema.org/InStock\",\n          validFrom: \"2024-05-21T12:00\",\n        }}\n        performer={{\n          \"@type\": \"PerformingGroup\",\n          name: \"Kira and Morrison\",\n        }}\n        organizer={{\n          \"@type\": \"Organization\",\n          name: \"Kira and Morrison Music\",\n          url: \"https://kiraandmorrisonmusic.com\",\n        }}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1 className=\"text-3xl font-bold mb-6\">\n          The Adventures of Kira and Morrison\n        </h1>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6 mb-8\">\n          <div className=\"bg-gray-100 p-4 rounded\">\n            <h2 className=\"text-xl font-semibold mb-2\">Event Details</h2>\n            <p>\n              <strong>Date:</strong> July 21, 2025\n            </p>\n            <p>\n              <strong>Time:</strong> 7:00 PM - 11:00 PM (EST)\n            </p>\n            <p>\n              <strong>Venue:</strong> Snickerpark Stadium\n            </p>\n            <p>\n              <strong>Address:</strong> 100 West Snickerpark Dr, Snickertown, PA\n              19019\n            </p>\n          </div>\n\n          <div className=\"bg-gray-100 p-4 rounded\">\n            <h2 className=\"text-xl font-semibold mb-2\">Ticket Information</h2>\n            <p>\n              <strong>Price:</strong> $30 USD\n            </p>\n            <p>\n              <strong>Availability:</strong> In Stock\n            </p>\n            <p>\n              <strong>On Sale:</strong> May 21, 2024 at 12:00 PM\n            </p>\n            <a\n              href=\"https://www.example.com/event_offer/12345_202403180430\"\n              className=\"inline-block mt-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700\"\n            >\n              Buy Tickets\n            </a>\n          </div>\n        </div>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">About the Event</h2>\n          <p>\n            The Adventures of Kira and Morrison is coming to Snickertown in a\n            can&apos;t miss performance. Join us for an unforgettable evening of\n            music and entertainment at the iconic Snickerpark Stadium.\n          </p>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Performers</h2>\n          <p>\n            <strong>Kira and Morrison</strong> - The dynamic duo returns to the\n            stage with their latest tour, featuring songs from their new album\n            and fan favorites.\n          </p>\n        </section>\n\n        <section>\n          <h2 className=\"text-2xl font-semibold mb-4\">Organized By</h2>\n          <p>\n            <strong>Kira and Morrison Music</strong>\n            <br />\n            Visit:{\" \"}\n            <a\n              href=\"https://kiraandmorrisonmusic.com\"\n              className=\"text-blue-600 hover:underline\"\n            >\n              kiraandmorrisonmusic.com\n            </a>\n          </p>\n        </section>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/event-cancelled/page.tsx",
    "content": "import { EventJsonLd } from \"next-seo\";\n\nexport default function EventCancelledPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <EventJsonLd\n        name=\"Summer Music Festival 2025\"\n        startDate=\"2025-08-15T12:00:00-05:00\"\n        endDate=\"2025-08-17T23:00:00-05:00\"\n        location={{\n          \"@type\": \"Place\",\n          name: \"City Park Amphitheater\",\n          address: {\n            \"@type\": \"PostalAddress\",\n            streetAddress: \"500 Park Avenue\",\n            addressLocality: \"Austin\",\n            addressRegion: \"TX\",\n            postalCode: \"78701\",\n            addressCountry: \"US\",\n          },\n        }}\n        eventStatus=\"https://schema.org/EventCancelled\"\n        description=\"The annual Summer Music Festival featuring multiple artists across three days.\"\n        image=\"https://example.com/summer-festival-2025.jpg\"\n        offers={{\n          \"@type\": \"Offer\",\n          url: \"https://example.com/summer-festival-tickets\",\n          price: 150,\n          priceCurrency: \"USD\",\n          availability: \"https://schema.org/SoldOut\",\n        }}\n        organizer=\"Austin Music Events\"\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <div className=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6\">\n          <h1 className=\"text-3xl font-bold mb-2\">EVENT CANCELLED</h1>\n          <p className=\"text-lg\">\n            Summer Music Festival 2025 has been cancelled. All tickets will be\n            refunded.\n          </p>\n        </div>\n\n        <h2 className=\"text-2xl font-semibold mb-4\">\n          Summer Music Festival 2025\n        </h2>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6 mb-8\">\n          <div className=\"bg-gray-100 p-4 rounded opacity-60\">\n            <h3 className=\"text-xl font-semibold mb-2\">\n              Original Event Details\n            </h3>\n            <p>\n              <strong>Date:</strong> August 15-17, 2025\n            </p>\n            <p>\n              <strong>Venue:</strong> City Park Amphitheater\n            </p>\n            <p>\n              <strong>Location:</strong> Austin, TX\n            </p>\n            <p className=\"text-red-600 font-semibold mt-2\">Status: CANCELLED</p>\n          </div>\n\n          <div className=\"bg-gray-100 p-4 rounded\">\n            <h3 className=\"text-xl font-semibold mb-2\">Refund Information</h3>\n            <p>\n              All ticket holders will receive a full refund within 7-10 business\n              days.\n            </p>\n            <p className=\"mt-2\">\n              For questions about refunds, please contact:\n              <br />\n              Email: refunds@austinmusicevents.com\n              <br />\n              Phone: 1-800-REFUNDS\n            </p>\n          </div>\n        </div>\n\n        <section className=\"mb-8\">\n          <h3 className=\"text-xl font-semibold mb-4\">Cancellation Notice</h3>\n          <p>\n            We regret to inform you that the Summer Music Festival 2025 has been\n            cancelled due to unforeseen circumstances. We apologize for any\n            inconvenience this may cause and appreciate your understanding.\n          </p>\n          <p className=\"mt-4\">\n            All ticket purchases will be automatically refunded to the original\n            payment method. Please allow 7-10 business days for the refund to\n            appear on your statement.\n          </p>\n        </section>\n\n        <section>\n          <h3 className=\"text-xl font-semibold mb-4\">Stay Updated</h3>\n          <p>\n            Follow Austin Music Events for updates about future events and\n            festivals. We look forward to bringing you amazing musical\n            experiences in the future.\n          </p>\n        </section>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/event-free/page.tsx",
    "content": "import { EventJsonLd } from \"next-seo\";\n\nexport default function EventFreePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <EventJsonLd\n        name=\"Community Coding Workshop: Introduction to Web Development\"\n        startDate=\"2025-07-15T18:00:00-05:00\"\n        endDate=\"2025-07-15T20:00:00-05:00\"\n        location={{\n          \"@type\": \"Place\",\n          name: \"Downtown Public Library\",\n          address: {\n            \"@type\": \"PostalAddress\",\n            streetAddress: \"123 Main Street\",\n            addressLocality: \"Springfield\",\n            addressRegion: \"IL\",\n            postalCode: \"62701\",\n            addressCountry: \"US\",\n          },\n        }}\n        description=\"Free workshop for beginners interested in learning web development. No prior experience required!\"\n        eventStatus=\"https://schema.org/EventScheduled\"\n        image=\"https://example.com/coding-workshop.jpg\"\n        offers={{\n          \"@type\": \"Offer\",\n          url: \"https://example.com/register/coding-workshop\",\n          price: 0,\n          priceCurrency: \"USD\",\n          availability: \"https://schema.org/InStock\",\n        }}\n        performer=\"Sarah Johnson\"\n        organizer={{\n          \"@type\": \"Organization\",\n          name: \"Code for Community\",\n          url: \"https://codeforcommunity.org\",\n        }}\n        url=\"https://codeforcommunity.org/events/intro-web-dev\"\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <div className=\"bg-green-100 border border-green-400 text-green-800 px-4 py-3 rounded mb-6\">\n          <h1 className=\"text-3xl font-bold mb-2\">FREE EVENT</h1>\n          <p className=\"text-lg\">\n            No registration fee required! Open to all community members.\n          </p>\n        </div>\n\n        <h2 className=\"text-2xl font-semibold mb-4\">\n          Community Coding Workshop: Introduction to Web Development\n        </h2>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6 mb-8\">\n          <div className=\"bg-gray-100 p-4 rounded\">\n            <h3 className=\"text-xl font-semibold mb-2\">Event Details</h3>\n            <p>\n              <strong>Date:</strong> July 15, 2025\n            </p>\n            <p>\n              <strong>Time:</strong> 6:00 PM - 8:00 PM (CST)\n            </p>\n            <p>\n              <strong>Venue:</strong> Downtown Public Library\n            </p>\n            <p>\n              <strong>Address:</strong> 123 Main Street, Springfield, IL\n            </p>\n            <p className=\"text-green-600 font-semibold mt-2\">Admission: FREE</p>\n          </div>\n\n          <div className=\"bg-gray-100 p-4 rounded\">\n            <h3 className=\"text-xl font-semibold mb-2\">Registration</h3>\n            <p>\n              <strong>Cost:</strong> FREE (No fees)\n            </p>\n            <p>\n              <strong>Seats Available:</strong> Limited to 30\n            </p>\n            <p className=\"mt-2 text-sm\">\n              Registration required to reserve your spot.\n            </p>\n            <a\n              href=\"https://example.com/register/coding-workshop\"\n              className=\"inline-block mt-2 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700\"\n            >\n              Register Now (Free)\n            </a>\n          </div>\n        </div>\n\n        <section className=\"mb-8\">\n          <h3 className=\"text-xl font-semibold mb-4\">About the Workshop</h3>\n          <p>\n            Join us for a free, beginner-friendly workshop on web development!\n            This two-hour session will introduce you to the basics of HTML, CSS,\n            and JavaScript. No prior programming experience is required.\n          </p>\n          <p className=\"mt-4\">\n            Perfect for anyone interested in learning to code, whether\n            you&apos;re exploring a career change, looking to build your own\n            website, or just curious about how the web works.\n          </p>\n        </section>\n\n        <section className=\"mb-8\">\n          <h3 className=\"text-xl font-semibold mb-4\">What You&apos;ll Learn</h3>\n          <ul className=\"list-disc pl-6\">\n            <li>Basic HTML structure and tags</li>\n            <li>Introduction to CSS styling</li>\n            <li>JavaScript fundamentals</li>\n            <li>Creating your first web page</li>\n            <li>Resources for continued learning</li>\n          </ul>\n        </section>\n\n        <section className=\"mb-8\">\n          <h3 className=\"text-xl font-semibold mb-4\">What to Bring</h3>\n          <ul className=\"list-disc pl-6\">\n            <li>Laptop (if you have one - we have a few loaners available)</li>\n            <li>Enthusiasm to learn!</li>\n            <li>Questions about web development</li>\n          </ul>\n          <p className=\"mt-4 text-sm italic\">\n            Light refreshments will be provided.\n          </p>\n        </section>\n\n        <section className=\"mb-8\">\n          <h3 className=\"text-xl font-semibold mb-4\">Instructor</h3>\n          <p>\n            <strong>Sarah Johnson</strong> is a senior web developer with over\n            10 years of experience and a passion for teaching. She volunteers\n            regularly with Code for Community to help make tech education\n            accessible to everyone.\n          </p>\n        </section>\n\n        <section>\n          <h3 className=\"text-xl font-semibold mb-4\">\n            About Code for Community\n          </h3>\n          <p>\n            Code for Community is a non-profit organization dedicated to\n            providing free coding education and resources to underserved\n            communities. Learn more at{\" \"}\n            <a\n              href=\"https://codeforcommunity.org\"\n              className=\"text-blue-600 hover:underline\"\n            >\n              codeforcommunity.org\n            </a>\n          </p>\n        </section>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/event-rescheduled/page.tsx",
    "content": "import { EventJsonLd } from \"next-seo\";\n\nexport default function EventRescheduledPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <EventJsonLd\n        name=\"Tech Conference 2025: Future of AI\"\n        startDate=\"2025-09-20T09:00:00-07:00\"\n        endDate=\"2025-09-22T17:00:00-07:00\"\n        location={{\n          \"@type\": \"Place\",\n          name: \"San Francisco Convention Center\",\n          address: {\n            \"@type\": \"PostalAddress\",\n            streetAddress: \"747 Howard Street\",\n            addressLocality: \"San Francisco\",\n            addressRegion: \"CA\",\n            postalCode: \"94103\",\n            addressCountry: \"US\",\n          },\n        }}\n        eventStatus=\"https://schema.org/EventRescheduled\"\n        previousStartDate={[\n          \"2025-03-15T09:00:00-07:00\",\n          \"2025-06-10T09:00:00-07:00\",\n        ]}\n        description=\"Join industry leaders for three days of cutting-edge AI discussions, workshops, and networking.\"\n        image={[\n          \"https://example.com/tech-conf-2025-16x9.jpg\",\n          \"https://example.com/tech-conf-2025-4x3.jpg\",\n          \"https://example.com/tech-conf-2025-1x1.jpg\",\n        ]}\n        offers={[\n          {\n            \"@type\": \"Offer\",\n            url: \"https://example.com/tickets/early-bird\",\n            price: 299,\n            priceCurrency: \"USD\",\n            availability: \"https://schema.org/InStock\",\n            validFrom: \"2025-01-01T00:00:00\",\n          },\n          {\n            \"@type\": \"Offer\",\n            url: \"https://example.com/tickets/vip\",\n            price: 599,\n            priceCurrency: \"USD\",\n            availability: \"https://schema.org/InStock\",\n            validFrom: \"2025-01-01T00:00:00\",\n          },\n        ]}\n        performer={[\n          {\n            \"@type\": \"Person\",\n            name: \"Dr. Sarah Chen\",\n            description: \"AI Research Director at TechCorp\",\n          },\n          {\n            \"@type\": \"Person\",\n            name: \"John Martinez\",\n            description: \"CEO of FutureAI\",\n          },\n          \"Panel of Industry Experts\",\n        ]}\n        organizer={{\n          \"@type\": \"Organization\",\n          name: \"TechEvents International\",\n          url: \"https://techevents.com\",\n        }}\n        url=\"https://techconf2025.com\"\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <div className=\"bg-yellow-100 border border-yellow-400 text-yellow-800 px-4 py-3 rounded mb-6\">\n          <h1 className=\"text-3xl font-bold mb-2\">EVENT RESCHEDULED</h1>\n          <p className=\"text-lg\">\n            New Date: September 20-22, 2025 (Originally scheduled for March\n            15-17, then June 10-12)\n          </p>\n        </div>\n\n        <h2 className=\"text-2xl font-semibold mb-4\">\n          Tech Conference 2025: Future of AI\n        </h2>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6 mb-8\">\n          <div className=\"bg-gray-100 p-4 rounded\">\n            <h3 className=\"text-xl font-semibold mb-2\">New Event Details</h3>\n            <p>\n              <strong>Date:</strong> September 20-22, 2025\n            </p>\n            <p>\n              <strong>Time:</strong> 9:00 AM - 5:00 PM (PST)\n            </p>\n            <p>\n              <strong>Venue:</strong> San Francisco Convention Center\n            </p>\n            <p>\n              <strong>Address:</strong> 747 Howard Street, San Francisco, CA\n            </p>\n            <p className=\"text-yellow-600 font-semibold mt-2\">\n              Status: RESCHEDULED\n            </p>\n          </div>\n\n          <div className=\"bg-gray-100 p-4 rounded\">\n            <h3 className=\"text-xl font-semibold mb-2\">Ticket Information</h3>\n            <p>\n              <strong>Early Bird:</strong> $299 USD\n            </p>\n            <p>\n              <strong>VIP Pass:</strong> $599 USD\n            </p>\n            <p className=\"mt-2 text-sm\">\n              All previously purchased tickets remain valid for the new dates.\n            </p>\n            <a\n              href=\"https://techconf2025.com\"\n              className=\"inline-block mt-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700\"\n            >\n              Learn More\n            </a>\n          </div>\n        </div>\n\n        <section className=\"mb-8 bg-blue-50 p-4 rounded\">\n          <h3 className=\"text-xl font-semibold mb-4\">Important Notice</h3>\n          <p>\n            Due to venue availability conflicts, Tech Conference 2025 has been\n            rescheduled from its original dates. This is the second reschedule:\n          </p>\n          <ul className=\"list-disc pl-6 mt-2\">\n            <li>Original Date: March 15-17, 2025</li>\n            <li>First Reschedule: June 10-12, 2025</li>\n            <li>\n              <strong>Final Date: September 20-22, 2025</strong>\n            </li>\n          </ul>\n          <p className=\"mt-4\">\n            All registered attendees have been notified via email. Your tickets\n            remain valid for the new dates. If you cannot attend, full refunds\n            are available until August 1, 2025.\n          </p>\n        </section>\n\n        <section className=\"mb-8\">\n          <h3 className=\"text-xl font-semibold mb-4\">Featured Speakers</h3>\n          <ul className=\"list-disc pl-6\">\n            <li>\n              <strong>Dr. Sarah Chen</strong> - AI Research Director at TechCorp\n            </li>\n            <li>\n              <strong>John Martinez</strong> - CEO of FutureAI\n            </li>\n            <li>Panel discussions with industry experts</li>\n          </ul>\n        </section>\n\n        <section className=\"mb-8\">\n          <h3 className=\"text-xl font-semibold mb-4\">About the Conference</h3>\n          <p>\n            Join industry leaders for three days of cutting-edge AI discussions,\n            workshops, and networking. Explore the latest developments in\n            artificial intelligence, machine learning, and their applications\n            across industries.\n          </p>\n        </section>\n\n        <section>\n          <h3 className=\"text-xl font-semibold mb-4\">Contact Information</h3>\n          <p>\n            For questions about the reschedule or refunds:\n            <br />\n            Email: support@techconf2025.com\n            <br />\n            Phone: 1-800-TECH-CONF\n            <br />\n            Website:{\" \"}\n            <a\n              href=\"https://techconf2025.com\"\n              className=\"text-blue-600 hover:underline\"\n            >\n              techconf2025.com\n            </a>\n          </p>\n        </section>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/faq/page.tsx",
    "content": "import { FAQJsonLd } from \"next-seo\";\n\nexport default function FAQPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <FAQJsonLd\n        questions={[\n          {\n            question: \"How to find an apprenticeship?\",\n            answer:\n              \"We provide an official service to search through available apprenticeships. To get started, create an account here, specify the desired region, and your preferences. You will be able to search through all officially registered open apprenticeships.\",\n          },\n          {\n            question: \"Whom to contact?\",\n            answer:\n              \"You can contact the apprenticeship office through our official phone hotline above, or with the web-form below. We generally respond to written requests within 7-10 days.\",\n          },\n          {\n            question: \"What are the requirements?\",\n            answer:\n              \"You must be at least 18 years old and have a high school diploma or equivalent. Additional requirements may vary depending on the specific apprenticeship program.\",\n          },\n          {\n            question: \"How long does the program last?\",\n            answer:\n              \"Most apprenticeship programs last between 2-4 years, depending on the trade and level of certification sought. The exact duration will be specified for each program.\",\n          },\n        ]}\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <h1>Frequently Asked Questions</h1>\n\n        <div className=\"mt-8 space-y-6\">\n          <div className=\"border-b pb-4\">\n            <h2 className=\"text-xl font-semibold mb-2\">\n              How to find an apprenticeship?\n            </h2>\n            <p className=\"text-gray-700\">\n              We provide an official service to search through available\n              apprenticeships. To get started, create an account here, specify\n              the desired region, and your preferences. You will be able to\n              search through all officially registered open apprenticeships.\n            </p>\n          </div>\n\n          <div className=\"border-b pb-4\">\n            <h2 className=\"text-xl font-semibold mb-2\">Whom to contact?</h2>\n            <p className=\"text-gray-700\">\n              You can contact the apprenticeship office through our official\n              phone hotline above, or with the web-form below. We generally\n              respond to written requests within 7-10 days.\n            </p>\n          </div>\n\n          <div className=\"border-b pb-4\">\n            <h2 className=\"text-xl font-semibold mb-2\">\n              What are the requirements?\n            </h2>\n            <p className=\"text-gray-700\">\n              You must be at least 18 years old and have a high school diploma\n              or equivalent. Additional requirements may vary depending on the\n              specific apprenticeship program.\n            </p>\n          </div>\n\n          <div className=\"border-b pb-4\">\n            <h2 className=\"text-xl font-semibold mb-2\">\n              How long does the program last?\n            </h2>\n            <p className=\"text-gray-700\">\n              Most apprenticeship programs last between 2-4 years, depending on\n              the trade and level of certification sought. The exact duration\n              will be specified for each program.\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/faq-advanced/page.tsx",
    "content": "import { FAQJsonLd } from \"next-seo\";\n\nexport default function AdvancedFAQPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <FAQJsonLd\n        questions={[\n          {\n            question: \"What documents are required for application?\",\n            answer: `<p>You'll need to provide the following documents:</p>\n<ul>\n  <li>Valid government-issued ID</li>\n  <li>High school diploma or equivalent</li>\n  <li>Proof of residence</li>\n  <li><a href=\"/forms/medical\">Medical clearance form</a></li>\n</ul>\n<p>All documents must be submitted within 30 days of application.</p>`,\n          },\n          {\n            question: \"How do I apply for the program?\",\n            answer: `<p>Follow these <strong>simple steps</strong> to apply:</p>\n<ol>\n  <li>Create an account on our <a href=\"/register\">registration page</a></li>\n  <li>Complete the online application form</li>\n  <li>Upload all required documents</li>\n  <li>Submit your application and pay the processing fee</li>\n  <li>Wait for confirmation email (usually within 24 hours)</li>\n</ol>\n<p><em>Note: Applications are processed in the order they are received.</em></p>`,\n          },\n          {\n            question: \"What financial assistance is available?\",\n            answer: `<p>We offer several types of financial assistance:</p>\n<div>\n  <h3>Scholarships</h3>\n  <p>Merit-based scholarships covering up to <strong>100% of tuition</strong></p>\n  \n  <h3>Grants</h3>\n  <p>Need-based grants ranging from <strong>$500 to $5,000</strong></p>\n  \n  <h3>Work-Study Programs</h3>\n  <p>Part-time employment opportunities to help offset costs</p>\n</div>\n<p>Visit our <a href=\"/financial-aid\">financial aid page</a> for more information.</p>`,\n          },\n          {\n            name: \"Are there any special requirements for international students?\",\n            acceptedAnswer: {\n              \"@type\": \"Answer\",\n              text: `<p>International students must meet additional requirements:</p>\n<ul>\n  <li><strong>English proficiency:</strong> TOEFL score of 80+ or IELTS 6.5+</li>\n  <li><strong>Visa documentation:</strong> Valid student visa (F-1 or M-1)</li>\n  <li><strong>Financial proof:</strong> Bank statements showing sufficient funds</li>\n  <li><strong>Health insurance:</strong> Comprehensive coverage required</li>\n</ul>\n<p>Contact our <a href=\"mailto:international@example.com\">international student office</a> for assistance.</p>`,\n            },\n          },\n        ]}\n        scriptId=\"advanced-faq-jsonld\"\n        scriptKey=\"faq-advanced\"\n      />\n\n      <div className=\"prose lg:prose-xl max-w-none\">\n        <h1>Advanced FAQ Example - Application Process</h1>\n        <p className=\"lead\">\n          This page demonstrates FAQ structured data with rich HTML content\n          including links, lists, and formatting.\n        </p>\n\n        <div className=\"mt-8 space-y-8\">\n          <details className=\"border rounded-lg p-4\" open>\n            <summary className=\"cursor-pointer font-semibold text-xl\">\n              What documents are required for application?\n            </summary>\n            <div className=\"mt-4\">\n              <p>You'll need to provide the following documents:</p>\n              <ul className=\"list-disc ml-6 mt-2\">\n                <li>Valid government-issued ID</li>\n                <li>High school diploma or equivalent</li>\n                <li>Proof of residence</li>\n                <li>\n                  <a href=\"/forms/medical\" className=\"text-blue-600 underline\">\n                    Medical clearance form\n                  </a>\n                </li>\n              </ul>\n              <p className=\"mt-4\">\n                All documents must be submitted within 30 days of application.\n              </p>\n            </div>\n          </details>\n\n          <details className=\"border rounded-lg p-4\">\n            <summary className=\"cursor-pointer font-semibold text-xl\">\n              How do I apply for the program?\n            </summary>\n            <div className=\"mt-4\">\n              <p>\n                Follow these <strong>simple steps</strong> to apply:\n              </p>\n              <ol className=\"list-decimal ml-6 mt-2\">\n                <li>\n                  Create an account on our{\" \"}\n                  <a href=\"/register\" className=\"text-blue-600 underline\">\n                    registration page\n                  </a>\n                </li>\n                <li>Complete the online application form</li>\n                <li>Upload all required documents</li>\n                <li>Submit your application and pay the processing fee</li>\n                <li>Wait for confirmation email (usually within 24 hours)</li>\n              </ol>\n              <p className=\"mt-4 italic\">\n                Note: Applications are processed in the order they are received.\n              </p>\n            </div>\n          </details>\n\n          <details className=\"border rounded-lg p-4\">\n            <summary className=\"cursor-pointer font-semibold text-xl\">\n              What financial assistance is available?\n            </summary>\n            <div className=\"mt-4\">\n              <p>We offer several types of financial assistance:</p>\n              <div className=\"mt-4\">\n                <h3 className=\"font-semibold text-lg\">Scholarships</h3>\n                <p>\n                  Merit-based scholarships covering up to{\" \"}\n                  <strong>100% of tuition</strong>\n                </p>\n\n                <h3 className=\"font-semibold text-lg mt-4\">Grants</h3>\n                <p>\n                  Need-based grants ranging from <strong>$500 to $5,000</strong>\n                </p>\n\n                <h3 className=\"font-semibold text-lg mt-4\">\n                  Work-Study Programs\n                </h3>\n                <p>Part-time employment opportunities to help offset costs</p>\n              </div>\n              <p className=\"mt-4\">\n                Visit our{\" \"}\n                <a href=\"/financial-aid\" className=\"text-blue-600 underline\">\n                  financial aid page\n                </a>{\" \"}\n                for more information.\n              </p>\n            </div>\n          </details>\n\n          <details className=\"border rounded-lg p-4\">\n            <summary className=\"cursor-pointer font-semibold text-xl\">\n              Are there any special requirements for international students?\n            </summary>\n            <div className=\"mt-4\">\n              <p>International students must meet additional requirements:</p>\n              <ul className=\"list-disc ml-6 mt-2\">\n                <li>\n                  <strong>English proficiency:</strong> TOEFL score of 80+ or\n                  IELTS 6.5+\n                </li>\n                <li>\n                  <strong>Visa documentation:</strong> Valid student visa (F-1\n                  or M-1)\n                </li>\n                <li>\n                  <strong>Financial proof:</strong> Bank statements showing\n                  sufficient funds\n                </li>\n                <li>\n                  <strong>Health insurance:</strong> Comprehensive coverage\n                  required\n                </li>\n              </ul>\n              <p className=\"mt-4\">\n                Contact our{\" \"}\n                <a\n                  href=\"mailto:international@example.com\"\n                  className=\"text-blue-600 underline\"\n                >\n                  international student office\n                </a>{\" \"}\n                for assistance.\n              </p>\n            </div>\n          </details>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/faq-health/page.tsx",
    "content": "import { FAQJsonLd } from \"next-seo\";\n\nexport default function HealthFAQPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <FAQJsonLd\n        questions={[\n          {\n            question: \"What are the symptoms of COVID-19?\",\n            answer: `<p>The most common symptoms of COVID-19 include:</p>\n<ul>\n  <li>Fever or chills</li>\n  <li>Cough</li>\n  <li>Shortness of breath or difficulty breathing</li>\n  <li>Fatigue</li>\n  <li>Muscle or body aches</li>\n  <li>Loss of taste or smell</li>\n</ul>\n<p>If you experience any of these symptoms, <strong>contact your healthcare provider immediately</strong>.</p>`,\n          },\n          {\n            question: \"How do I schedule a vaccination appointment?\",\n            answer: `<p>To schedule your vaccination appointment:</p>\n<ol>\n  <li>Visit our <a href=\"/vaccine-scheduler\">online scheduling portal</a></li>\n  <li>Call our hotline at <strong>1-800-VACCINE</strong></li>\n  <li>Visit any participating pharmacy or health center</li>\n</ol>\n<p>Appointments are available <em>Monday through Saturday, 8 AM to 6 PM</em>.</p>`,\n          },\n          {\n            question: \"What health insurance plans are accepted?\",\n            answer: `<p>We accept most major health insurance plans including:</p>\n<ul>\n  <li>Medicare (Parts A, B, C, and D)</li>\n  <li>Medicaid</li>\n  <li>Blue Cross Blue Shield</li>\n  <li>Aetna</li>\n  <li>UnitedHealthcare</li>\n  <li>Cigna</li>\n</ul>\n<p>For uninsured patients, we offer <strong>sliding scale fees</strong> based on income. Contact our <a href=\"/financial-assistance\">financial assistance office</a> for more information.</p>`,\n          },\n          {\n            question: \"What preventive health screenings are recommended?\",\n            answer: `<p>Recommended preventive health screenings vary by age and risk factors:</p>\n<div>\n  <h4>Ages 18-39:</h4>\n  <ul>\n    <li>Blood pressure check every 2 years</li>\n    <li>Cholesterol check every 5 years</li>\n    <li>Annual dental exam</li>\n  </ul>\n  \n  <h4>Ages 40-65:</h4>\n  <ul>\n    <li>Annual blood pressure and cholesterol checks</li>\n    <li>Diabetes screening every 3 years</li>\n    <li>Cancer screenings as recommended</li>\n  </ul>\n  \n  <h4>Ages 65+:</h4>\n  <ul>\n    <li>All previous screenings plus bone density test</li>\n    <li>Annual cognitive assessment</li>\n    <li>Fall risk assessment</li>\n  </ul>\n</div>\n<p>Consult with your healthcare provider for personalized recommendations.</p>`,\n          },\n          {\n            question: \"How can I access my medical records online?\",\n            answer: `<p>Access your medical records through our secure patient portal:</p>\n<ol>\n  <li>Go to <a href=\"/patient-portal\">our patient portal</a></li>\n  <li>Log in with your username and password</li>\n  <li>Navigate to \"Medical Records\" section</li>\n  <li>View, download, or print your records</li>\n</ol>\n<p><strong>First-time users:</strong> You'll need your patient ID and date of birth to register. For assistance, call <strong>1-800-RECORDS</strong>.</p>`,\n          },\n        ]}\n      />\n\n      <div className=\"prose lg:prose-xl max-w-none\">\n        <div className=\"bg-blue-50 p-4 rounded-lg mb-8\">\n          <p className=\"text-sm text-blue-800\">\n            <strong>Note:</strong> This is an example of a health-focused FAQ\n            page. In production, FAQ rich results are only available for\n            well-known, authoritative government or health websites as\n            determined by Google.\n          </p>\n        </div>\n\n        <h1>Health Services FAQ</h1>\n        <p className=\"lead\">\n          Find answers to common questions about our health services, insurance,\n          and patient resources.\n        </p>\n\n        <div className=\"mt-8 space-y-6\">\n          <div className=\"bg-white shadow rounded-lg p-6\">\n            <h2 className=\"text-xl font-semibold mb-3\">\n              What are the symptoms of COVID-19?\n            </h2>\n            <div className=\"text-gray-700\">\n              <p>The most common symptoms of COVID-19 include:</p>\n              <ul className=\"list-disc ml-6 mt-2\">\n                <li>Fever or chills</li>\n                <li>Cough</li>\n                <li>Shortness of breath or difficulty breathing</li>\n                <li>Fatigue</li>\n                <li>Muscle or body aches</li>\n                <li>Loss of taste or smell</li>\n              </ul>\n              <p className=\"mt-3\">\n                If you experience any of these symptoms,{\" \"}\n                <strong>contact your healthcare provider immediately</strong>.\n              </p>\n            </div>\n          </div>\n\n          <div className=\"bg-white shadow rounded-lg p-6\">\n            <h2 className=\"text-xl font-semibold mb-3\">\n              How do I schedule a vaccination appointment?\n            </h2>\n            <div className=\"text-gray-700\">\n              <p>To schedule your vaccination appointment:</p>\n              <ol className=\"list-decimal ml-6 mt-2\">\n                <li>\n                  Visit our{\" \"}\n                  <a\n                    href=\"/vaccine-scheduler\"\n                    className=\"text-blue-600 underline\"\n                  >\n                    online scheduling portal\n                  </a>\n                </li>\n                <li>\n                  Call our hotline at <strong>1-800-VACCINE</strong>\n                </li>\n                <li>Visit any participating pharmacy or health center</li>\n              </ol>\n              <p className=\"mt-3 italic\">\n                Appointments are available Monday through Saturday, 8 AM to 6\n                PM.\n              </p>\n            </div>\n          </div>\n\n          <div className=\"bg-white shadow rounded-lg p-6\">\n            <h2 className=\"text-xl font-semibold mb-3\">\n              What health insurance plans are accepted?\n            </h2>\n            <div className=\"text-gray-700\">\n              <p>We accept most major health insurance plans including:</p>\n              <ul className=\"list-disc ml-6 mt-2\">\n                <li>Medicare (Parts A, B, C, and D)</li>\n                <li>Medicaid</li>\n                <li>Blue Cross Blue Shield</li>\n                <li>Aetna</li>\n                <li>UnitedHealthcare</li>\n                <li>Cigna</li>\n              </ul>\n              <p className=\"mt-3\">\n                For uninsured patients, we offer{\" \"}\n                <strong>sliding scale fees</strong> based on income. Contact our{\" \"}\n                <a\n                  href=\"/financial-assistance\"\n                  className=\"text-blue-600 underline\"\n                >\n                  financial assistance office\n                </a>{\" \"}\n                for more information.\n              </p>\n            </div>\n          </div>\n\n          <div className=\"bg-white shadow rounded-lg p-6\">\n            <h2 className=\"text-xl font-semibold mb-3\">\n              What preventive health screenings are recommended?\n            </h2>\n            <div className=\"text-gray-700\">\n              <p>\n                Recommended preventive health screenings vary by age and risk\n                factors:\n              </p>\n              <div className=\"mt-3\">\n                <h4 className=\"font-semibold\">Ages 18-39:</h4>\n                <ul className=\"list-disc ml-6 mb-3\">\n                  <li>Blood pressure check every 2 years</li>\n                  <li>Cholesterol check every 5 years</li>\n                  <li>Annual dental exam</li>\n                </ul>\n\n                <h4 className=\"font-semibold\">Ages 40-65:</h4>\n                <ul className=\"list-disc ml-6 mb-3\">\n                  <li>Annual blood pressure and cholesterol checks</li>\n                  <li>Diabetes screening every 3 years</li>\n                  <li>Cancer screenings as recommended</li>\n                </ul>\n\n                <h4 className=\"font-semibold\">Ages 65+:</h4>\n                <ul className=\"list-disc ml-6 mb-3\">\n                  <li>All previous screenings plus bone density test</li>\n                  <li>Annual cognitive assessment</li>\n                  <li>Fall risk assessment</li>\n                </ul>\n              </div>\n              <p className=\"mt-3\">\n                Consult with your healthcare provider for personalized\n                recommendations.\n              </p>\n            </div>\n          </div>\n\n          <div className=\"bg-white shadow rounded-lg p-6\">\n            <h2 className=\"text-xl font-semibold mb-3\">\n              How can I access my medical records online?\n            </h2>\n            <div className=\"text-gray-700\">\n              <p>\n                Access your medical records through our secure patient portal:\n              </p>\n              <ol className=\"list-decimal ml-6 mt-2\">\n                <li>\n                  Go to{\" \"}\n                  <a href=\"/patient-portal\" className=\"text-blue-600 underline\">\n                    our patient portal\n                  </a>\n                </li>\n                <li>Log in with your username and password</li>\n                <li>Navigate to \"Medical Records\" section</li>\n                <li>View, download, or print your records</li>\n              </ol>\n              <p className=\"mt-3\">\n                <strong>First-time users:</strong> You'll need your patient ID\n                and date of birth to register. For assistance, call{\" \"}\n                <strong>1-800-RECORDS</strong>.\n              </p>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/globals.css",
    "content": ":root {\n  --background: #ffffff;\n  --foreground: #171717;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --background: #0a0a0a;\n    --foreground: #ededed;\n  }\n}\n\nhtml,\nbody {\n  max-width: 100vw;\n  overflow-x: hidden;\n}\n\nbody {\n  color: var(--foreground);\n  background: var(--background);\n  font-family: Arial, Helvetica, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n* {\n  box-sizing: border-box;\n  padding: 0;\n  margin: 0;\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\n@media (prefers-color-scheme: dark) {\n  html {\n    color-scheme: dark;\n  }\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/howto/page.tsx",
    "content": "import { HowToJsonLd } from \"next-seo\";\n\nexport default function HowToPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <HowToJsonLd\n        name=\"How to Change a Flat Tire\"\n        description=\"Step-by-step guide to safely change a flat tire on the roadside\"\n        image=\"https://example.com/images/tire-change.jpg\"\n        estimatedCost=\"$20\"\n        prepTime=\"PT5M\"\n        performTime=\"PT25M\"\n        totalTime=\"PT30M\"\n        yield=\"1 changed tire\"\n        tool={[\"Spare tire\", \"Lug wrench\", \"Jack\", \"Wheel wedges\"]}\n        supply={[\"Flares\"]}\n        step={[\n          \"Turn on your hazard lights and apply parking brake\",\n          \"Apply wheel wedges behind the tires\",\n          \"Remove the hubcap and loosen the lug nuts\",\n          \"Place the jack under the vehicle frame\",\n          \"Raise the vehicle until the flat tire is off the ground\",\n          \"Remove the lug nuts and the flat tire\",\n          \"Mount the spare tire and hand-tighten the lug nuts\",\n          \"Lower the vehicle and fully tighten the lug nuts\",\n          \"Check the tire pressure and drive to a service station\",\n        ]}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>How to Change a Flat Tire</h1>\n        <p className=\"text-gray-600\">\n          A step-by-step guide to safely changing a flat tire\n        </p>\n\n        <div className=\"bg-yellow-50 border-l-4 border-yellow-400 p-4 my-4\">\n          <p className=\"font-medium\">Safety First</p>\n          <p>\n            Always pull over to a safe location away from traffic before\n            attempting to change a tire.\n          </p>\n        </div>\n\n        <h2>What You'll Need</h2>\n        <div className=\"grid grid-cols-2 gap-4\">\n          <div>\n            <h3>Tools</h3>\n            <ul>\n              <li>Spare tire</li>\n              <li>Lug wrench</li>\n              <li>Jack</li>\n              <li>Wheel wedges</li>\n            </ul>\n          </div>\n          <div>\n            <h3>Supplies</h3>\n            <ul>\n              <li>Flares or reflective triangles</li>\n            </ul>\n          </div>\n        </div>\n\n        <h2>Time Required</h2>\n        <p>\n          <strong>Preparation:</strong> 5 minutes\n          <br />\n          <strong>Performing the task:</strong> 25 minutes\n          <br />\n          <strong>Total time:</strong> 30 minutes\n        </p>\n\n        <h2>Estimated Cost</h2>\n        <p>$20 (for flares and other emergency supplies)</p>\n\n        <h2>Step-by-Step Instructions</h2>\n        <ol>\n          <li>\n            <strong>Secure the vehicle:</strong> Turn on your hazard lights and\n            apply the parking brake.\n          </li>\n          <li>\n            <strong>Prevent rolling:</strong> Apply wheel wedges behind the\n            tires to prevent the vehicle from moving.\n          </li>\n          <li>\n            <strong>Prepare the wheel:</strong> Remove the hubcap and loosen the\n            lug nuts (turn counterclockwise) while the tire is still on the\n            ground.\n          </li>\n          <li>\n            <strong>Position the jack:</strong> Place the jack under the vehicle\n            frame near the flat tire.\n          </li>\n          <li>\n            <strong>Raise the vehicle:</strong> Use the jack to raise the\n            vehicle until the flat tire is about 6 inches off the ground.\n          </li>\n          <li>\n            <strong>Remove the flat:</strong> Remove the lug nuts completely and\n            pull off the flat tire.\n          </li>\n          <li>\n            <strong>Mount the spare:</strong> Place the spare tire on the lug\n            bolts and hand-tighten the lug nuts.\n          </li>\n          <li>\n            <strong>Lower and tighten:</strong> Lower the vehicle and fully\n            tighten the lug nuts in a star pattern.\n          </li>\n          <li>\n            <strong>Final check:</strong> Check the tire pressure and drive to a\n            service station to have the spare properly inspected.\n          </li>\n        </ol>\n\n        <div className=\"bg-gray-100 p-4 rounded-lg my-8\">\n          <h3>Pro Tips</h3>\n          <ul>\n            <li>Keep your spare tire inflated and check it periodically</li>\n            <li>Store a flashlight in your trunk for nighttime emergencies</li>\n            <li>\n              Most spare tires are not designed for high speeds - drive under 50\n              mph\n            </li>\n          </ul>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/howto-advanced/page.tsx",
    "content": "import { HowToJsonLd } from \"next-seo\";\n\nexport default function HowToAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <HowToJsonLd\n        name=\"How to Change a Flat Tire\"\n        description=\"Complete guide to safely changing a flat tire on the roadside with detailed sections\"\n        image={{\n          url: \"https://example.com/images/tire-change-guide.jpg\",\n          width: 1200,\n          height: 800,\n        }}\n        estimatedCost={{\n          currency: \"USD\",\n          value: 20,\n        }}\n        prepTime=\"PT5M\"\n        performTime=\"PT25M\"\n        totalTime=\"PT30M\"\n        yield=\"1 changed tire\"\n        tool={[\n          {\n            name: \"Spare tire\",\n          },\n          {\n            name: \"Lug wrench\",\n            image: \"https://example.com/images/lug-wrench.jpg\",\n          },\n          {\n            name: \"Jack\",\n          },\n          {\n            name: \"Wheel wedges\",\n            image: \"https://example.com/images/wheel-wedges.jpg\",\n          },\n        ]}\n        supply={[\n          {\n            name: \"Flares\",\n            image: \"https://example.com/images/flares.jpg\",\n          },\n        ]}\n        step={[\n          {\n            \"@type\": \"HowToSection\",\n            name: \"Preparation\",\n            position: 1,\n            itemListElement: [\n              {\n                \"@type\": \"HowToStep\",\n                position: 1,\n                itemListElement: [\n                  {\n                    \"@type\": \"HowToDirection\",\n                    position: 1,\n                    text: \"Turn on your hazard lights and set the flares.\",\n                  },\n                  {\n                    \"@type\": \"HowToTip\",\n                    position: 2,\n                    text: \"You're going to need space and want to be visible.\",\n                  },\n                ],\n              },\n              {\n                \"@type\": \"HowToStep\",\n                position: 2,\n                itemListElement: [\n                  {\n                    \"@type\": \"HowToDirection\",\n                    position: 1,\n                    text: \"Position your wheel wedges in front of the front tires if a rear tire is flat, or behind the rear tires if a front tire is flat.\",\n                  },\n                  {\n                    \"@type\": \"HowToTip\",\n                    position: 2,\n                    text: \"You don't want the car to move while you're working on it.\",\n                  },\n                ],\n              },\n            ],\n          },\n          {\n            \"@type\": \"HowToSection\",\n            name: \"Raise the car\",\n            position: 2,\n            itemListElement: [\n              {\n                \"@type\": \"HowToStep\",\n                position: 1,\n                text: \"Position the jack underneath the car, next to the flat tire.\",\n                image: \"https://example.com/images/position-jack.jpg\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                position: 2,\n                itemListElement: [\n                  {\n                    \"@type\": \"HowToDirection\",\n                    position: 1,\n                    text: \"Raise the jack until the flat tire is just barely off of the ground.\",\n                    beforeMedia: \"https://example.com/images/car-on-ground.jpg\",\n                    afterMedia: \"https://example.com/images/car-raised.jpg\",\n                  },\n                  {\n                    \"@type\": \"HowToTip\",\n                    position: 2,\n                    text: \"It doesn't need to be too high.\",\n                  },\n                ],\n              },\n              {\n                \"@type\": \"HowToStep\",\n                position: 3,\n                text: \"Remove the hubcap and loosen the lug nuts.\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                position: 4,\n                text: \"Remove the flat tire and put the spare tire on the exposed lug bolts.\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                position: 5,\n                itemListElement: [\n                  {\n                    \"@type\": \"HowToDirection\",\n                    position: 1,\n                    text: \"Tighten the lug nuts by hand.\",\n                  },\n                  {\n                    \"@type\": \"HowToTip\",\n                    position: 2,\n                    text: \"Don't use the wrench just yet.\",\n                  },\n                ],\n              },\n            ],\n          },\n          {\n            \"@type\": \"HowToSection\",\n            name: \"Finishing up\",\n            position: 3,\n            itemListElement: [\n              {\n                \"@type\": \"HowToStep\",\n                position: 1,\n                text: \"Lower the jack and tighten the lug nuts with the wrench.\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                position: 2,\n                text: \"Replace the hubcap.\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                position: 3,\n                text: \"Put the equipment and the flat tire away.\",\n              },\n            ],\n          },\n        ]}\n        video={{\n          name: \"How to Change a Tire Video Tutorial\",\n          description:\n            \"Watch our mechanic demonstrate the proper technique for changing a flat tire\",\n          thumbnailUrl: \"https://example.com/video/tire-change-thumb.jpg\",\n          contentUrl: \"https://example.com/video/tire-change-tutorial.mp4\",\n          uploadDate: \"2024-01-15T08:00:00+00:00\",\n          duration: \"PT8M30S\",\n        }}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>How to Change a Flat Tire</h1>\n        <p className=\"text-gray-600\">\n          A comprehensive guide with detailed sections and helpful tips\n        </p>\n\n        <div className=\"bg-blue-50 border-l-4 border-blue-400 p-4 my-4\">\n          <p className=\"font-medium\">Estimated Time: 30 minutes</p>\n          <p>Preparation: 5 minutes | Performing: 25 minutes</p>\n        </div>\n\n        <h2>Section 1: Preparation</h2>\n        <ol>\n          <li>\n            <strong>Set up safety signals:</strong> Turn on your hazard lights\n            and set out flares or reflective triangles.\n            <div className=\"text-sm text-gray-500 mt-1\">\n              💡 Tip: You need space and want to be visible to other drivers.\n            </div>\n          </li>\n          <li>\n            <strong>Secure the vehicle:</strong> Position wheel wedges in front\n            of front tires (for rear flat) or behind rear tires (for front\n            flat).\n            <div className=\"text-sm text-gray-500 mt-1\">\n              💡 Tip: This prevents the car from moving while you work.\n            </div>\n          </li>\n        </ol>\n\n        <h2>Section 2: Raise the Car</h2>\n        <ol>\n          <li>\n            <strong>Position the jack:</strong> Place it underneath the car,\n            next to the flat tire.\n          </li>\n          <li>\n            <strong>Raise the vehicle:</strong> Lift until the flat tire is\n            barely off the ground.\n            <div className=\"text-sm text-gray-500 mt-1\">\n              💡 Tip: It doesn't need to be too high - just enough to clear the\n              ground.\n            </div>\n          </li>\n          <li>\n            <strong>Remove hubcap:</strong> Take off the hubcap and loosen the\n            lug nuts.\n          </li>\n          <li>\n            <strong>Swap the tire:</strong> Remove the flat and mount the spare\n            on the lug bolts.\n          </li>\n          <li>\n            <strong>Hand-tighten:</strong> Tighten the lug nuts by hand first.\n            <div className=\"text-sm text-gray-500 mt-1\">\n              💡 Tip: Don't use the wrench yet - wait until the car is lowered.\n            </div>\n          </li>\n        </ol>\n\n        <h2>Section 3: Finishing Up</h2>\n        <ol>\n          <li>\n            <strong>Lower and tighten:</strong> Lower the jack and use the\n            wrench to fully tighten lug nuts.\n          </li>\n          <li>\n            <strong>Replace hubcap:</strong> Put the hubcap back on if your\n            spare uses one.\n          </li>\n          <li>\n            <strong>Store equipment:</strong> Put away the jack, wrench, and\n            flat tire.\n          </li>\n        </ol>\n\n        <div className=\"bg-gray-100 p-4 rounded-lg my-8\">\n          <h3>Video Tutorial</h3>\n          <p>\n            Watch our 8-minute video tutorial to see a professional mechanic\n            demonstrate the proper technique.\n          </p>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/image/page.tsx",
    "content": "import { ImageJsonLd } from \"next-seo\";\n\nexport default function ImagePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ImageJsonLd\n        contentUrl=\"https://example.com/photos/black-labrador-puppy.jpg\"\n        creator=\"Brixton Brownstone\"\n        license=\"https://example.com/license\"\n        acquireLicensePage=\"https://example.com/how-to-use-my-images\"\n        creditText=\"Labrador PhotoLab\"\n        copyrightNotice=\"Clara Kent\"\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-3xl font-bold mb-6\">Black Labrador Puppy</h1>\n\n        <div className=\"bg-gray-100 p-6 rounded-lg\">\n          <h2 className=\"text-xl font-semibold mb-4\">Image Information</h2>\n          <dl className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n            <div>\n              <dt className=\"font-medium text-gray-700\">Photographer</dt>\n              <dd className=\"text-gray-900\">Brixton Brownstone</dd>\n            </div>\n            <div>\n              <dt className=\"font-medium text-gray-700\">Copyright</dt>\n              <dd className=\"text-gray-900\">Clara Kent</dd>\n            </div>\n            <div>\n              <dt className=\"font-medium text-gray-700\">Credit</dt>\n              <dd className=\"text-gray-900\">Labrador PhotoLab</dd>\n            </div>\n            <div>\n              <dt className=\"font-medium text-gray-700\">License</dt>\n              <dd>\n                <a\n                  href=\"https://example.com/license\"\n                  className=\"text-blue-600 hover:underline\"\n                >\n                  View License\n                </a>\n              </dd>\n            </div>\n          </dl>\n\n          <div className=\"mt-6\">\n            <a\n              href=\"https://example.com/how-to-use-my-images\"\n              className=\"inline-block bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700\"\n            >\n              How to Use My Images\n            </a>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/image-advanced/page.tsx",
    "content": "import { ImageJsonLd } from \"next-seo\";\n\nexport default function ImageAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ImageJsonLd\n        contentUrl=\"https://example.com/photos/sunset-landscape.jpg\"\n        creator={{\n          name: \"PhotoLab Studios\",\n          logo: \"https://example.com/photolab-logo.jpg\",\n          sameAs: [\n            \"https://twitter.com/photolab\",\n            \"https://instagram.com/photolab\",\n            \"https://facebook.com/photolab\",\n          ],\n          address: {\n            streetAddress: \"123 Photography Lane\",\n            addressLocality: \"San Francisco\",\n            addressRegion: \"CA\",\n            postalCode: \"94105\",\n            addressCountry: \"US\",\n          },\n        }}\n        license=\"https://creativecommons.org/licenses/by-nc/4.0/\"\n        acquireLicensePage=\"https://example.com/licensing/premium\"\n        creditText=\"PhotoLab Studios - Professional Photography\"\n        copyrightNotice=\"© 2024 PhotoLab Studios. All rights reserved.\"\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-3xl font-bold mb-6\">\n          Sunset Landscape - Premium Photography\n        </h1>\n\n        <div className=\"mb-8 bg-gray-200 rounded-lg p-16 text-center\">\n          <p className=\"text-gray-600\">Image placeholder: Sunset Landscape</p>\n        </div>\n\n        <div className=\"bg-gray-100 p-6 rounded-lg mb-6\">\n          <h2 className=\"text-xl font-semibold mb-4\">Studio Information</h2>\n          <div className=\"flex items-start space-x-4 mb-4\">\n            <div className=\"w-16 h-16 rounded bg-gray-300 flex items-center justify-center\">\n              <span className=\"text-gray-600 text-xs\">Logo</span>\n            </div>\n            <div>\n              <h3 className=\"font-semibold\">PhotoLab Studios</h3>\n              <p className=\"text-gray-600\">\n                Professional Photography Since 2010\n              </p>\n              <p className=\"text-sm text-gray-500\">\n                123 Photography Lane, San Francisco, CA 94105\n              </p>\n            </div>\n          </div>\n\n          <div className=\"flex space-x-4 mb-4\">\n            <a\n              href=\"https://twitter.com/photolab\"\n              className=\"text-blue-400 hover:underline\"\n            >\n              Twitter\n            </a>\n            <a\n              href=\"https://instagram.com/photolab\"\n              className=\"text-pink-600 hover:underline\"\n            >\n              Instagram\n            </a>\n            <a\n              href=\"https://facebook.com/photolab\"\n              className=\"text-blue-600 hover:underline\"\n            >\n              Facebook\n            </a>\n          </div>\n        </div>\n\n        <div className=\"bg-white border border-gray-200 p-6 rounded-lg\">\n          <h2 className=\"text-xl font-semibold mb-4\">Licensing Information</h2>\n\n          <div className=\"mb-4\">\n            <h3 className=\"font-medium text-gray-700 mb-2\">License Type</h3>\n            <p className=\"text-gray-900\">Creative Commons BY-NC 4.0</p>\n            <a\n              href=\"https://creativecommons.org/licenses/by-nc/4.0/\"\n              className=\"text-blue-600 hover:underline text-sm\"\n            >\n              View full license terms\n            </a>\n          </div>\n\n          <div className=\"mb-4\">\n            <h3 className=\"font-medium text-gray-700 mb-2\">Copyright</h3>\n            <p className=\"text-gray-900\">\n              © 2024 PhotoLab Studios. All rights reserved.\n            </p>\n          </div>\n\n          <div className=\"mb-4\">\n            <h3 className=\"font-medium text-gray-700 mb-2\">Credit Line</h3>\n            <p className=\"text-gray-900\">\n              PhotoLab Studios - Professional Photography\n            </p>\n          </div>\n\n          <div className=\"mt-6 space-y-3\">\n            <a\n              href=\"https://example.com/licensing/premium\"\n              className=\"block text-center bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700\"\n            >\n              License This Image for Commercial Use\n            </a>\n            <p className=\"text-sm text-gray-600 text-center\">\n              Need a different license? Contact us for custom licensing options.\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/image-multiple/page.tsx",
    "content": "import { ImageJsonLd } from \"next-seo\";\n\nexport default function ImageMultiplePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ImageJsonLd\n        images={[\n          {\n            contentUrl: \"https://example.com/photos/mountain-sunrise.jpg\",\n            creator: \"Alex Mountain\",\n            license: \"https://example.com/license/standard\",\n            creditText: \"Nature Photography Collection\",\n            copyrightNotice: \"© 2024 Alex Mountain\",\n          },\n          {\n            contentUrl: \"https://example.com/photos/ocean-waves.jpg\",\n            creator: [\n              \"Sarah Ocean\",\n              {\n                name: \"Coastal Studios\",\n                url: \"https://coastalstudios.com\",\n              },\n            ],\n            license: \"https://creativecommons.org/licenses/by-sa/4.0/\",\n            acquireLicensePage:\n              \"https://example.com/licensing/ocean-collection\",\n            creditText: \"Coastal Studios & Sarah Ocean\",\n            copyrightNotice: \"© 2024 Coastal Studios\",\n          },\n          {\n            contentUrl: \"https://example.com/photos/city-lights.jpg\",\n            creator: {\n              name: \"Urban Photography Inc.\",\n              logo: \"https://example.com/urban-photo-logo.jpg\",\n              sameAs: [\"https://instagram.com/urbanphoto\"],\n            },\n            license: \"https://example.com/license/commercial\",\n            acquireLicensePage: \"https://example.com/licensing/urban\",\n            creditText: \"Urban Photography Inc.\",\n            copyrightNotice:\n              \"© 2024 Urban Photography Inc. All rights reserved.\",\n          },\n        ]}\n      />\n\n      <div className=\"max-w-6xl mx-auto\">\n        <h1 className=\"text-3xl font-bold mb-8\">\n          Photography Collection Gallery\n        </h1>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n          {/* Mountain Sunrise */}\n          <div className=\"bg-white rounded-lg shadow-lg overflow-hidden\">\n            <div className=\"w-full h-48 bg-gray-200 flex items-center justify-center\">\n              <p className=\"text-gray-600\">Mountain Sunrise</p>\n            </div>\n            <div className=\"p-4\">\n              <h3 className=\"font-semibold mb-2\">Mountain Sunrise</h3>\n              <dl className=\"text-sm space-y-1\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Photographer:</dt>\n                  <dd className=\"font-medium\">Alex Mountain</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Credit:</dt>\n                  <dd className=\"font-medium\">Nature Photography Collection</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Copyright:</dt>\n                  <dd className=\"font-medium\">© 2024 Alex Mountain</dd>\n                </div>\n              </dl>\n              <a\n                href=\"https://example.com/license/standard\"\n                className=\"inline-block mt-3 text-blue-600 hover:underline text-sm\"\n              >\n                View License\n              </a>\n            </div>\n          </div>\n\n          {/* Ocean Waves */}\n          <div className=\"bg-white rounded-lg shadow-lg overflow-hidden\">\n            <div className=\"w-full h-48 bg-gray-200 flex items-center justify-center\">\n              <p className=\"text-gray-600\">Ocean Waves</p>\n            </div>\n            <div className=\"p-4\">\n              <h3 className=\"font-semibold mb-2\">Ocean Waves</h3>\n              <dl className=\"text-sm space-y-1\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Creators:</dt>\n                  <dd className=\"font-medium\">Sarah Ocean & Coastal Studios</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Credit:</dt>\n                  <dd className=\"font-medium\">Coastal Studios & Sarah Ocean</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">License:</dt>\n                  <dd className=\"font-medium\">CC BY-SA 4.0</dd>\n                </div>\n              </dl>\n              <div className=\"mt-3 space-x-3\">\n                <a\n                  href=\"https://creativecommons.org/licenses/by-sa/4.0/\"\n                  className=\"text-blue-600 hover:underline text-sm\"\n                >\n                  License\n                </a>\n                <a\n                  href=\"https://example.com/licensing/ocean-collection\"\n                  className=\"text-blue-600 hover:underline text-sm\"\n                >\n                  Get Commercial License\n                </a>\n              </div>\n            </div>\n          </div>\n\n          {/* City Lights */}\n          <div className=\"bg-white rounded-lg shadow-lg overflow-hidden\">\n            <div className=\"w-full h-48 bg-gray-200 flex items-center justify-center\">\n              <p className=\"text-gray-600\">City Lights</p>\n            </div>\n            <div className=\"p-4\">\n              <h3 className=\"font-semibold mb-2\">City Lights</h3>\n              <div className=\"flex items-center space-x-2 mb-2\">\n                <div className=\"w-8 h-8 rounded bg-gray-300 flex items-center justify-center\">\n                  <span className=\"text-gray-600 text-xs\">U</span>\n                </div>\n                <span className=\"font-medium text-sm\">\n                  Urban Photography Inc.\n                </span>\n              </div>\n              <dl className=\"text-sm space-y-1\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Credit:</dt>\n                  <dd className=\"font-medium\">Urban Photography Inc.</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Copyright:</dt>\n                  <dd className=\"font-medium\">© 2024 All rights reserved</dd>\n                </div>\n              </dl>\n              <div className=\"mt-3 space-x-3\">\n                <a\n                  href=\"https://example.com/license/commercial\"\n                  className=\"text-blue-600 hover:underline text-sm\"\n                >\n                  License\n                </a>\n                <a\n                  href=\"https://example.com/licensing/urban\"\n                  className=\"text-blue-600 hover:underline text-sm\"\n                >\n                  Purchase\n                </a>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-8 p-6 bg-gray-100 rounded-lg\">\n          <h2 className=\"text-xl font-semibold mb-3\">About This Collection</h2>\n          <p className=\"text-gray-700 mb-4\">\n            This gallery showcases various photography styles from different\n            creators and studios. Each image has its own licensing terms and\n            creators. Some images are available under Creative Commons licenses,\n            while others require commercial licensing.\n          </p>\n          <p className=\"text-sm text-gray-600\">\n            To use any of these images, please review the individual license\n            terms and contact the respective creators or studios for permissions\n            beyond the stated licenses.\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/job-posting/page.tsx",
    "content": "import { JobPostingJsonLd } from \"next-seo\";\n\nexport default function JobPostingPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>We are looking for a passionate Software Engineer to design, develop and install software solutions.</p><p>The successful candidate will be able to build high-quality, innovative and fully performing software in compliance with coding standards and technical design.</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Tech Solutions Inc.\"\n        jobLocation=\"San Francisco, CA\"\n        validThrough=\"2024-03-18T00:00\"\n        employmentType=\"FULL_TIME\"\n        baseSalary={{\n          currency: \"USD\",\n          value: {\n            minValue: 90000,\n            maxValue: 120000,\n            unitText: \"YEAR\",\n          },\n        }}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Software Engineer</h1>\n        <div className=\"text-gray-600 mb-4\">\n          <p>Tech Solutions Inc. • San Francisco, CA</p>\n          <p>Posted: January 18, 2024 • Expires: March 18, 2024</p>\n          <p>$90,000 - $120,000 per year</p>\n        </div>\n\n        <h2>Job Description</h2>\n        <p>\n          We are looking for a passionate Software Engineer to design, develop\n          and install software solutions. The successful candidate will be able\n          to build high-quality, innovative and fully performing software in\n          compliance with coding standards and technical design.\n        </p>\n\n        <h2>Responsibilities</h2>\n        <ul>\n          <li>Execute full software development life cycle (SDLC)</li>\n          <li>\n            Develop flowcharts, layouts and documentation to identify\n            requirements and solutions\n          </li>\n          <li>Write well-designed, testable code</li>\n          <li>Produce specifications and determine operational feasibility</li>\n          <li>\n            Integrate software components into a fully functional software\n            system\n          </li>\n        </ul>\n\n        <h2>Requirements</h2>\n        <ul>\n          <li>Proven experience as a Software Engineer or similar role</li>\n          <li>\n            Ability to develop software in Java, Ruby, C++ or other programming\n            languages\n          </li>\n          <li>\n            Excellent knowledge of relational databases, SQL and ORM\n            technologies\n          </li>\n          <li>\n            Experience developing web applications using at least one popular\n            web framework\n          </li>\n          <li>BSc degree in Computer Science, Engineering or relevant field</li>\n        </ul>\n\n        <button className=\"bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600\">\n          Apply Now\n        </button>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/job-posting-advanced/page.tsx",
    "content": "import { JobPostingJsonLd } from \"next-seo\";\n\nexport default function AdvancedJobPostingPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <JobPostingJsonLd\n        title=\"Senior Product Manager\"\n        description=\"<p>Google is seeking an experienced Product Manager to lead our Cloud Platform initiatives.</p><p><strong>About the role:</strong></p><ul><li>Lead product strategy for Google Cloud Platform</li><li>Work with engineering teams across multiple locations</li><li>Define roadmap and deliver innovative solutions</li></ul><p><strong>Requirements:</strong></p><ul><li>7+ years of product management experience</li><li>Experience with cloud technologies</li><li>MBA or equivalent practical experience</li></ul>\"\n        datePosted=\"2024-01-18\"\n        validThrough=\"2024-04-18T23:59:59\"\n        hiringOrganization={{\n          name: \"Google\",\n          sameAs: \"https://www.google.com\",\n          logo: {\n            url: \"https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png\",\n            width: 272,\n            height: 92,\n          },\n        }}\n        jobLocation={[\n          {\n            address: {\n              streetAddress: \"1600 Amphitheatre Parkway\",\n              addressLocality: \"Mountain View\",\n              addressRegion: \"CA\",\n              postalCode: \"94043\",\n              addressCountry: \"US\",\n            },\n          },\n          {\n            address: {\n              streetAddress: \"111 8th Avenue\",\n              addressLocality: \"New York\",\n              addressRegion: \"NY\",\n              postalCode: \"10011\",\n              addressCountry: \"US\",\n            },\n          },\n        ]}\n        jobLocationType=\"TELECOMMUTE\"\n        applicantLocationRequirements={[\n          { name: \"California, USA\" },\n          { name: \"New York, USA\" },\n          { name: \"Washington, USA\" },\n          { name: \"Texas, USA\" },\n        ]}\n        url=\"https://careers.google.com/jobs/senior-product-manager-cloud\"\n        employmentType={[\"FULL_TIME\", \"CONTRACTOR\"]}\n        identifier={{\n          name: \"Google\",\n          value: \"GCP-PM-2024-001\",\n        }}\n        baseSalary={{\n          currency: \"USD\",\n          value: {\n            minValue: 180000,\n            maxValue: 280000,\n            unitText: \"YEAR\",\n          },\n        }}\n        directApply={true}\n        educationRequirements={[\n          {\n            credentialCategory: \"bachelor degree\",\n          },\n          {\n            credentialCategory: \"postgraduate degree\",\n          },\n        ]}\n        experienceRequirements={{\n          monthsOfExperience: 84,\n        }}\n        experienceInPlaceOfEducation={true}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Senior Product Manager - Cloud Platform</h1>\n        <div className=\"text-gray-600 mb-4\">\n          <p>Google • Mountain View, CA / New York, NY / Remote</p>\n          <p>Posted: January 18, 2024 • Expires: April 18, 2024</p>\n          <p>$180,000 - $280,000 per year</p>\n          <div className=\"flex gap-2 mt-2\">\n            <span className=\"bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm\">\n              Full-time\n            </span>\n            <span className=\"bg-purple-100 text-purple-800 px-2 py-1 rounded text-sm\">\n              Contract Available\n            </span>\n            <span className=\"bg-green-100 text-green-800 px-2 py-1 rounded text-sm\">\n              Hybrid/Remote Options\n            </span>\n          </div>\n        </div>\n\n        <h2>About the Role</h2>\n        <p>\n          Google is seeking an experienced Product Manager to lead our Cloud\n          Platform initiatives. This is a unique opportunity to shape the future\n          of cloud computing at scale.\n        </p>\n\n        <h2>Responsibilities</h2>\n        <ul>\n          <li>Lead product strategy for Google Cloud Platform services</li>\n          <li>Work with engineering teams across multiple locations</li>\n          <li>Define roadmap and deliver innovative solutions</li>\n          <li>Collaborate with Sales, Marketing, and Partner teams</li>\n          <li>Analyze market trends and competitive landscape</li>\n          <li>Drive product launches and go-to-market strategies</li>\n        </ul>\n\n        <h2>Minimum Qualifications</h2>\n        <ul>\n          <li>\n            Bachelor's degree in Computer Science, Engineering, or related field\n          </li>\n          <li>7+ years of product management experience</li>\n          <li>Experience with cloud technologies and enterprise software</li>\n          <li>Track record of launching successful products</li>\n        </ul>\n\n        <h2>Preferred Qualifications</h2>\n        <ul>\n          <li>MBA or Master's degree in a technical field</li>\n          <li>Experience with AI/ML products</li>\n          <li>Previous experience at a major cloud provider</li>\n          <li>\n            Strong technical background with ability to engage with engineers\n          </li>\n        </ul>\n\n        <h2>Location & Work Arrangement</h2>\n        <p>\n          This role offers flexibility with options to work from our offices in\n          Mountain View or New York, or remotely from select states (CA, NY, WA,\n          TX). We support a hybrid work model with 3 days in office for those\n          near our locations.\n        </p>\n\n        <h2>Compensation & Benefits</h2>\n        <ul>\n          <li>Base salary range: $180,000 - $280,000</li>\n          <li>Equity compensation</li>\n          <li>Annual bonus potential</li>\n          <li>Comprehensive health coverage</li>\n          <li>401(k) matching</li>\n          <li>Professional development budget</li>\n        </ul>\n\n        <div className=\"mt-8 flex gap-4\">\n          <button className=\"bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600\">\n            Apply Now\n          </button>\n          <button className=\"border border-blue-500 text-blue-500 px-6 py-2 rounded hover:bg-blue-50\">\n            Save Job\n          </button>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/job-posting-remote/page.tsx",
    "content": "import { JobPostingJsonLd } from \"next-seo\";\n\nexport default function RemoteJobPostingPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <JobPostingJsonLd\n        title=\"Senior Frontend Developer\"\n        description=\"<p>Join our fully remote team to build amazing user experiences!</p><p>We're looking for an experienced Frontend Developer who is passionate about creating beautiful, performant web applications.</p><p>This is a 100% remote position open to candidates in the United States.</p>\"\n        datePosted=\"2024-01-18\"\n        validThrough=\"2024-02-28T00:00\"\n        hiringOrganization={{\n          name: \"Remote First Tech\",\n          sameAs: \"https://www.remotefirsttech.com\",\n          logo: \"https://www.remotefirsttech.com/logo.png\",\n        }}\n        jobLocationType=\"TELECOMMUTE\"\n        applicantLocationRequirements={{\n          name: \"USA\",\n        }}\n        url=\"https://careers.remotefirsttech.com/jobs/senior-frontend-dev\"\n        employmentType=\"FULL_TIME\"\n        identifier=\"RFT-2024-001\"\n        baseSalary={{\n          currency: \"USD\",\n          value: {\n            value: 130000,\n            unitText: \"YEAR\",\n          },\n        }}\n        directApply={true}\n        educationRequirements=\"bachelor degree\"\n        experienceRequirements={{\n          monthsOfExperience: 60,\n        }}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Senior Frontend Developer</h1>\n        <div className=\"text-gray-600 mb-4\">\n          <p>Remote First Tech • Remote (USA)</p>\n          <p>Posted: January 18, 2024 • Expires: February 28, 2024</p>\n          <p>$130,000 per year</p>\n          <span className=\"bg-green-100 text-green-800 px-2 py-1 rounded text-sm\">\n            🏠 100% Remote\n          </span>\n        </div>\n\n        <h2>About the Role</h2>\n        <p>\n          Join our fully remote team to build amazing user experiences! We're\n          looking for an experienced Frontend Developer who is passionate about\n          creating beautiful, performant web applications.\n        </p>\n        <p>\n          This is a 100% remote position open to candidates in the United\n          States.\n        </p>\n\n        <h2>What You'll Do</h2>\n        <ul>\n          <li>Build and maintain complex React applications</li>\n          <li>Collaborate with designers to implement pixel-perfect UIs</li>\n          <li>Optimize applications for maximum speed and scalability</li>\n          <li>\n            Mentor junior developers and contribute to technical decisions\n          </li>\n          <li>Work closely with backend engineers to design APIs</li>\n        </ul>\n\n        <h2>What We're Looking For</h2>\n        <ul>\n          <li>5+ years of experience in frontend development</li>\n          <li>Expert-level knowledge of React, TypeScript, and modern CSS</li>\n          <li>Experience with state management (Redux, Zustand, etc.)</li>\n          <li>Strong understanding of web performance optimization</li>\n          <li>\n            Bachelor's degree in Computer Science or equivalent experience\n          </li>\n          <li>Excellent communication skills for remote collaboration</li>\n        </ul>\n\n        <h2>Benefits</h2>\n        <ul>\n          <li>100% remote work from anywhere in the USA</li>\n          <li>Flexible working hours</li>\n          <li>Health, dental, and vision insurance</li>\n          <li>$1,500 home office setup budget</li>\n          <li>Annual learning & development budget</li>\n          <li>Quarterly team retreats</li>\n        </ul>\n\n        <button className=\"bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600\">\n          Apply Now\n        </button>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/jsonld-test-page/page.tsx",
    "content": "// examples/app-router-showcase/app/jsonld-test-page/page.tsx\nimport React from \"react\";\n// Assuming JsonLdScript is exported from your library's entry point\n// and you've linked it via pnpm workspace: add next-seo@workspace:*\nimport { JsonLdScript } from \"next-seo\"; // Or the correct path if not yet fully packaged\n\nconst testData = {\n  \"@context\": \"https://schema.org\",\n  \"@type\": \"WebPage\", // Example type\n  name: \"E2E Test Page for JSON-LD\",\n  description: \"This page tests the JsonLdScript component.\",\n  url: \"http://localhost:3001/jsonld-test-page\",\n};\n\nexport default function JsonLdTestPage() {\n  return (\n    <div>\n      <h1>JSON-LD Test Page</h1>\n      <p>This page includes a JSON-LD script for testing purposes.</p>\n      <JsonLdScript\n        data={testData}\n        id=\"e2e-jsonld-script\"\n        scriptKey=\"e2e-test\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Geist, Geist_Mono } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst geistSans = Geist({\n  variable: \"--font-geist-sans\",\n  subsets: [\"latin\"],\n});\n\nconst geistMono = Geist_Mono({\n  variable: \"--font-geist-mono\",\n  subsets: [\"latin\"],\n});\n\nexport const metadata: Metadata = {\n  title: \"Create Next App\",\n  description: \"Generated by create next app\",\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\">\n      <body className={`${geistSans.variable} ${geistMono.variable}`}>\n        {children}\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/local-business/page.tsx",
    "content": "import { LocalBusinessJsonLd } from \"next-seo\";\n\nexport default function LocalBusinessPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <LocalBusinessJsonLd\n        type=\"LocalBusiness\"\n        name=\"Gary's Tech Repair Shop\"\n        address={{\n          \"@type\": \"PostalAddress\",\n          streetAddress: \"123 Tech Street\",\n          addressLocality: \"San Francisco\",\n          addressRegion: \"CA\",\n          postalCode: \"94102\",\n          addressCountry: \"US\",\n        }}\n        telephone=\"+14155551234\"\n        url=\"https://example.com/locations/sf\"\n        description=\"Professional computer and phone repair services in San Francisco\"\n        openingHoursSpecification={[\n          {\n            \"@type\": \"OpeningHoursSpecification\",\n            dayOfWeek: [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"],\n            opens: \"09:00\",\n            closes: \"18:00\",\n          },\n          {\n            \"@type\": \"OpeningHoursSpecification\",\n            dayOfWeek: \"Saturday\",\n            opens: \"10:00\",\n            closes: \"16:00\",\n          },\n        ]}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Gary's Tech Repair Shop</h1>\n        <p>\n          Welcome to Gary's Tech Repair Shop, your trusted partner for all\n          computer and phone repair needs in San Francisco.\n        </p>\n\n        <h2>Our Services</h2>\n        <ul>\n          <li>Computer repair and upgrades</li>\n          <li>Phone screen replacement</li>\n          <li>Data recovery</li>\n          <li>Virus removal</li>\n        </ul>\n\n        <h2>Location & Hours</h2>\n        <address>\n          123 Tech Street\n          <br />\n          San Francisco, CA 94102\n          <br />\n          Phone: (415) 555-1234\n        </address>\n\n        <p>\n          <strong>Hours:</strong>\n          <br />\n          Monday-Friday: 9:00 AM - 6:00 PM\n          <br />\n          Saturday: 10:00 AM - 4:00 PM\n          <br />\n          Sunday: Closed\n        </p>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/merchant-return-policy/page.tsx",
    "content": "import { MerchantReturnPolicyJsonLd } from \"next-seo\";\n\nexport default function MerchantReturnPolicyPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <MerchantReturnPolicyJsonLd\n        applicableCountry={[\"US\", \"CA\"]}\n        returnPolicyCountry=\"US\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n        merchantReturnDays={30}\n        returnMethod=\"https://schema.org/ReturnByMail\"\n        returnFees=\"https://schema.org/FreeReturn\"\n        refundType=\"https://schema.org/FullRefund\"\n        returnLabelSource=\"https://schema.org/ReturnLabelDownloadAndPrint\"\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-6\">Return Policy</h1>\n\n        <section className=\"mb-8 bg-blue-50 p-6 rounded-lg\">\n          <h2 className=\"text-2xl font-semibold mb-4\">30-Day Return Policy</h2>\n          <ul className=\"space-y-2 text-gray-700\">\n            <li>✓ 30-day return window from delivery date</li>\n            <li>✓ Free returns by mail</li>\n            <li>✓ Full refund guaranteed</li>\n            <li>✓ Download and print return label</li>\n            <li>✓ Applicable in US and Canada</li>\n          </ul>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">How to Return</h2>\n          <ol className=\"list-decimal pl-6 space-y-2 text-gray-700\">\n            <li>Log into your account and select the order</li>\n            <li>Choose items to return and reason</li>\n            <li>Download and print the return label</li>\n            <li>Pack items securely with all original packaging</li>\n            <li>Drop off at any authorized shipping location</li>\n          </ol>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Refund Processing</h2>\n          <p className=\"text-gray-700\">\n            Once we receive your return, we'll inspect the items and process\n            your refund within 3-5 business days. The refund will be credited to\n            your original payment method.\n          </p>\n        </section>\n\n        <section className=\"text-sm text-gray-600\">\n          <p>\n            This example demonstrates a basic merchant return policy with a\n            30-day return window, free returns, and full refunds.\n          </p>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/merchant-return-policy-advanced/page.tsx",
    "content": "import { MerchantReturnPolicyJsonLd } from \"next-seo\";\n\nexport default function MerchantReturnPolicyAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <MerchantReturnPolicyJsonLd\n        applicableCountry={[\"DE\", \"AT\", \"CH\"]}\n        returnPolicyCountry=\"IE\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n        merchantReturnDays={60}\n        itemCondition={[\n          \"https://schema.org/NewCondition\",\n          \"https://schema.org/DamagedCondition\",\n        ]}\n        returnMethod={[\n          \"https://schema.org/ReturnByMail\",\n          \"https://schema.org/ReturnInStore\",\n        ]}\n        returnFees=\"https://schema.org/ReturnShippingFees\"\n        returnShippingFeesAmount={{\n          value: 2.99,\n          currency: \"EUR\",\n        }}\n        refundType={[\n          \"https://schema.org/FullRefund\",\n          \"https://schema.org/ExchangeRefund\",\n        ]}\n        restockingFee={{\n          value: 10,\n          currency: \"EUR\",\n        }}\n        returnLabelSource=\"https://schema.org/ReturnLabelInBox\"\n        customerRemorseReturnFees=\"https://schema.org/ReturnShippingFees\"\n        customerRemorseReturnShippingFeesAmount={{\n          value: 5.99,\n          currency: \"EUR\",\n        }}\n        customerRemorseReturnLabelSource=\"https://schema.org/ReturnLabelDownloadAndPrint\"\n        itemDefectReturnFees=\"https://schema.org/FreeReturn\"\n        itemDefectReturnLabelSource=\"https://schema.org/ReturnLabelInBox\"\n        returnPolicySeasonalOverride={{\n          startDate: \"2025-12-01\",\n          endDate: \"2025-01-05\",\n          returnPolicyCategory:\n            \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n          merchantReturnDays: 30,\n        }}\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-6\">Advanced Return Policy</h1>\n\n        <section className=\"mb-8 bg-green-50 p-6 rounded-lg\">\n          <h2 className=\"text-2xl font-semibold mb-4\">\n            Standard Policy (60 Days)\n          </h2>\n          <ul className=\"space-y-2 text-gray-700\">\n            <li>✓ 60-day return window for most items</li>\n            <li>✓ Return by mail or in-store</li>\n            <li>✓ €2.99 standard return shipping fee</li>\n            <li>✓ €10 restocking fee may apply</li>\n            <li>✓ Available in Germany, Austria, and Switzerland</li>\n            <li>✓ Returns processed in Ireland</li>\n          </ul>\n        </section>\n\n        <section className=\"mb-8 bg-yellow-50 p-6 rounded-lg\">\n          <h2 className=\"text-2xl font-semibold mb-4\">\n            Holiday Season Override\n          </h2>\n          <p className=\"font-medium mb-2\">December 1 - January 5</p>\n          <ul className=\"space-y-2 text-gray-700\">\n            <li>⚠️ Reduced to 30-day return window</li>\n            <li>⚠️ All other conditions remain the same</li>\n          </ul>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Return Conditions</h2>\n\n          <div className=\"mb-4\">\n            <h3 className=\"text-lg font-semibold mb-2\">\n              Customer Remorse Returns\n            </h3>\n            <ul className=\"pl-6 space-y-1 text-gray-700\">\n              <li>• Return shipping fee: €5.99</li>\n              <li>• Download and print return label</li>\n              <li>• Restocking fee applies</li>\n            </ul>\n          </div>\n\n          <div className=\"mb-4\">\n            <h3 className=\"text-lg font-semibold mb-2\">\n              Defective Item Returns\n            </h3>\n            <ul className=\"pl-6 space-y-1 text-gray-700\">\n              <li>• Free return shipping</li>\n              <li>• Return label included in box</li>\n              <li>• No restocking fee</li>\n              <li>• Priority processing</li>\n            </ul>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">\n            Acceptable Item Conditions\n          </h2>\n          <ul className=\"pl-6 space-y-1 text-gray-700\">\n            <li>• New, unopened items</li>\n            <li>• Damaged or defective items</li>\n          </ul>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Refund Options</h2>\n          <ul className=\"pl-6 space-y-1 text-gray-700\">\n            <li>• Full refund to original payment method</li>\n            <li>• Exchange for same or different product</li>\n          </ul>\n        </section>\n\n        <section className=\"text-sm text-gray-600\">\n          <p>\n            This example demonstrates an advanced merchant return policy with\n            seasonal overrides, different fees for customer remorse vs defects,\n            and multiple return methods.\n          </p>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/merchant-return-policy-link/page.tsx",
    "content": "import { MerchantReturnPolicyJsonLd } from \"next-seo\";\n\nexport default function MerchantReturnPolicyLinkPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <MerchantReturnPolicyJsonLd merchantReturnLink=\"https://www.example.com/returns\" />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-6\">Return Policy (Link Only)</h1>\n\n        <section className=\"mb-8 bg-blue-50 p-6 rounded-lg\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Our Return Policy</h2>\n          <p className=\"text-gray-700 mb-4\">\n            We maintain a comprehensive return policy to ensure customer\n            satisfaction. For complete details about our return process,\n            conditions, and procedures, please visit our dedicated returns page.\n          </p>\n          <a\n            href=\"https://www.example.com/returns\"\n            className=\"inline-block bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition-colors\"\n          >\n            View Full Return Policy →\n          </a>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Quick Overview</h2>\n          <ul className=\"space-y-2 text-gray-700\">\n            <li>✓ Hassle-free returns</li>\n            <li>✓ Multiple return options</li>\n            <li>✓ Clear refund process</li>\n            <li>✓ Customer support available</li>\n          </ul>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Need Help?</h2>\n          <p className=\"text-gray-700\">\n            If you have questions about returns or need assistance with a\n            return, our customer service team is here to help. Contact us at{\" \"}\n            <a\n              href=\"mailto:returns@example.com\"\n              className=\"text-blue-600 hover:underline\"\n            >\n              returns@example.com\n            </a>{\" \"}\n            or call 1-800-RETURNS.\n          </p>\n        </section>\n\n        <section className=\"text-sm text-gray-600\">\n          <p>\n            This example demonstrates the simple \"Option B\" approach where you\n            only provide a link to your return policy page instead of detailed\n            structured data. This is useful when your return policy is complex\n            or frequently updated.\n          </p>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/mobile-app/page.tsx",
    "content": "import { SoftwareApplicationJsonLd } from \"next-seo\";\n\nexport default function MobileAppPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <SoftwareApplicationJsonLd\n        type=\"MobileApplication\"\n        name=\"FitTrack - Fitness & Workout Tracker\"\n        description=\"Your personal fitness companion for tracking workouts, nutrition, and health goals\"\n        url=\"https://example.com/fittrack\"\n        image={[\n          \"https://example.com/fittrack-icon-1x1.png\",\n          {\n            url: \"https://example.com/fittrack-icon-4x3.png\",\n            width: 1200,\n            height: 900,\n          },\n          {\n            url: \"https://example.com/fittrack-icon-16x9.png\",\n            width: 1920,\n            height: 1080,\n          },\n        ]}\n        applicationCategory=\"HealthApplication\"\n        applicationSubCategory=\"FitnessTracking\"\n        operatingSystem=\"Android 7.0+, iOS 13.0+\"\n        offers={{\n          price: 0,\n          priceCurrency: \"USD\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.7,\n          ratingCount: 8500,\n          reviewCount: 6200,\n        }}\n        review={[\n          {\n            author: \"Alex Rivera\",\n            reviewRating: { ratingValue: 5 },\n            reviewBody:\n              \"This app transformed my fitness journey! The workout tracking is incredibly detailed.\",\n            datePublished: \"2024-11-10\",\n          },\n          {\n            author: \"Emma Thompson\",\n            reviewRating: { ratingValue: 4 },\n            reviewBody:\n              \"Great app with useful features. Would love to see more yoga workouts added.\",\n            datePublished: \"2024-10-28\",\n          },\n        ]}\n        permissions={[\n          \"android.permission.ACTIVITY_RECOGNITION\",\n          \"android.permission.ACCESS_FINE_LOCATION\",\n          \"android.permission.CAMERA\",\n          \"android.permission.READ_EXTERNAL_STORAGE\",\n          \"android.permission.RECEIVE_BOOT_COMPLETED\",\n        ]}\n        screenshot={[\n          {\n            url: \"https://example.com/screenshots/fittrack-dashboard.jpg\",\n            caption: \"Personal fitness dashboard\",\n          },\n          {\n            url: \"https://example.com/screenshots/fittrack-workout.jpg\",\n            caption: \"Workout tracking in progress\",\n          },\n          {\n            url: \"https://example.com/screenshots/fittrack-nutrition.jpg\",\n            caption: \"Nutrition tracking and meal planning\",\n          },\n          {\n            url: \"https://example.com/screenshots/fittrack-progress.jpg\",\n            caption: \"Progress charts and analytics\",\n          },\n        ]}\n        featureList={[\n          \"500+ exercise library with videos\",\n          \"Custom workout creation\",\n          \"GPS route tracking for runs\",\n          \"Calorie and macro tracking\",\n          \"Progress photos and measurements\",\n          \"Apple Health and Google Fit sync\",\n          \"Social challenges with friends\",\n          \"Offline mode support\",\n          \"Wearable device integration\",\n        ]}\n        softwareVersion=\"5.3.2\"\n        datePublished=\"2019-01-15\"\n        dateModified=\"2024-11-20\"\n        author=\"FitTech Solutions\"\n        publisher={{\n          name: \"FitTech Solutions Ltd.\",\n          url: \"https://fittechsolutions.com\",\n          logo: \"https://fittechsolutions.com/logo.png\",\n        }}\n        downloadUrl=\"https://play.google.com/store/apps/details?id=com.fittech.fittrack\"\n        installUrl=\"https://apps.apple.com/app/fittrack/id123456789\"\n        countriesSupported={[\"US\", \"CA\", \"GB\", \"AU\", \"NZ\", \"IE\", \"SG\", \"IN\"]}\n        storageRequirements=\"150MB\"\n      />\n\n      <div className=\"max-w-4xl\">\n        <div className=\"flex items-center space-x-4 mb-8\">\n          <div className=\"w-24 h-24 bg-gradient-to-br from-green-400 to-blue-500 rounded-2xl flex items-center justify-center\">\n            <span className=\"text-white text-4xl font-bold\">FT</span>\n          </div>\n          <div>\n            <h1 className=\"text-4xl font-bold\">FitTrack</h1>\n            <p className=\"text-xl text-gray-600\">Fitness & Workout Tracker</p>\n          </div>\n        </div>\n\n        <div className=\"bg-gradient-to-r from-green-50 to-blue-50 rounded-lg p-8 mb-8\">\n          <h2 className=\"text-2xl font-bold mb-4\">\n            Start Your Fitness Journey Today\n          </h2>\n          <p className=\"text-gray-700 mb-6\">\n            Join millions of users who have transformed their lives with\n            FitTrack. Track workouts, monitor nutrition, and achieve your health\n            goals.\n          </p>\n          <div className=\"flex flex-wrap gap-4\">\n            <a\n              href=\"#\"\n              className=\"inline-flex items-center bg-black text-white px-6 py-3 rounded-lg hover:bg-gray-800 transition\"\n            >\n              <svg\n                className=\"w-6 h-6 mr-2\"\n                viewBox=\"0 0 24 24\"\n                fill=\"currentColor\"\n              >\n                <path d=\"M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z\" />\n              </svg>\n              Download on App Store\n            </a>\n            <a\n              href=\"#\"\n              className=\"inline-flex items-center bg-black text-white px-6 py-3 rounded-lg hover:bg-gray-800 transition\"\n            >\n              <svg\n                className=\"w-6 h-6 mr-2\"\n                viewBox=\"0 0 24 24\"\n                fill=\"currentColor\"\n              >\n                <path d=\"M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-3.198l2.807 1.626a1 1 0 0 1 0 1.73l-2.808 1.626L15.206 12l2.492-2.491zM5.864 2.658L16.802 8.99l-2.303 2.303-8.635-8.635z\" />\n              </svg>\n              Get it on Google Play\n            </a>\n          </div>\n        </div>\n\n        <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4 mb-8\">\n          <div className=\"bg-white rounded-lg shadow p-4 text-center\">\n            <div className=\"text-3xl font-bold text-green-600\">4.7</div>\n            <div className=\"text-yellow-400\">★★★★★</div>\n            <div className=\"text-sm text-gray-600\">8,500 ratings</div>\n          </div>\n          <div className=\"bg-white rounded-lg shadow p-4 text-center\">\n            <div className=\"text-3xl font-bold text-blue-600\">5M+</div>\n            <div className=\"text-sm text-gray-600\">Downloads</div>\n          </div>\n          <div className=\"bg-white rounded-lg shadow p-4 text-center\">\n            <div className=\"text-3xl font-bold text-purple-600\">Free</div>\n            <div className=\"text-sm text-gray-600\">With premium option</div>\n          </div>\n          <div className=\"bg-white rounded-lg shadow p-4 text-center\">\n            <div className=\"text-3xl font-bold text-orange-600\">150MB</div>\n            <div className=\"text-sm text-gray-600\">App size</div>\n          </div>\n        </div>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Key Features</h2>\n          <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <div className=\"text-3xl mb-3\">🏋️‍♂️</div>\n              <h3 className=\"font-semibold mb-2\">Workout Tracking</h3>\n              <p className=\"text-gray-600 text-sm\">\n                500+ exercises with video guides, custom workout creation, and\n                real-time tracking\n              </p>\n            </div>\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <div className=\"text-3xl mb-3\">🍎</div>\n              <h3 className=\"font-semibold mb-2\">Nutrition Logger</h3>\n              <p className=\"text-gray-600 text-sm\">\n                Track calories and macros, barcode scanner, meal planning tools\n              </p>\n            </div>\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <div className=\"text-3xl mb-3\">📊</div>\n              <h3 className=\"font-semibold mb-2\">Progress Analytics</h3>\n              <p className=\"text-gray-600 text-sm\">\n                Detailed charts, progress photos, body measurements tracking\n              </p>\n            </div>\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <div className=\"text-3xl mb-3\">🏃‍♀️</div>\n              <h3 className=\"font-semibold mb-2\">GPS Tracking</h3>\n              <p className=\"text-gray-600 text-sm\">\n                Track runs, walks, and bike rides with route mapping\n              </p>\n            </div>\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <div className=\"text-3xl mb-3\">👥</div>\n              <h3 className=\"font-semibold mb-2\">Social Features</h3>\n              <p className=\"text-gray-600 text-sm\">\n                Challenge friends, join communities, share achievements\n              </p>\n            </div>\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <div className=\"text-3xl mb-3\">⌚</div>\n              <h3 className=\"font-semibold mb-2\">Wearable Sync</h3>\n              <p className=\"text-gray-600 text-sm\">\n                Connects with Apple Watch, Fitbit, Garmin, and more\n              </p>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Screenshots</h2>\n          <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n            <div className=\"bg-gray-200 rounded-lg aspect-[9/16] flex items-center justify-center\">\n              <span className=\"text-gray-500 text-sm\">Dashboard</span>\n            </div>\n            <div className=\"bg-gray-200 rounded-lg aspect-[9/16] flex items-center justify-center\">\n              <span className=\"text-gray-500 text-sm\">Workout</span>\n            </div>\n            <div className=\"bg-gray-200 rounded-lg aspect-[9/16] flex items-center justify-center\">\n              <span className=\"text-gray-500 text-sm\">Nutrition</span>\n            </div>\n            <div className=\"bg-gray-200 rounded-lg aspect-[9/16] flex items-center justify-center\">\n              <span className=\"text-gray-500 text-sm\">Progress</span>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Permissions</h2>\n          <div className=\"bg-gray-50 rounded-lg p-6\">\n            <p className=\"text-gray-700 mb-4\">\n              FitTrack requires the following permissions to provide the best\n              experience:\n            </p>\n            <ul className=\"space-y-2\">\n              <li className=\"flex items-start\">\n                <span className=\"text-green-500 mr-2\">📍</span>\n                <div>\n                  <span className=\"font-medium\">Location</span>\n                  <p className=\"text-sm text-gray-600\">\n                    For GPS tracking during outdoor activities\n                  </p>\n                </div>\n              </li>\n              <li className=\"flex items-start\">\n                <span className=\"text-green-500 mr-2\">📷</span>\n                <div>\n                  <span className=\"font-medium\">Camera</span>\n                  <p className=\"text-sm text-gray-600\">\n                    For progress photos and barcode scanning\n                  </p>\n                </div>\n              </li>\n              <li className=\"flex items-start\">\n                <span className=\"text-green-500 mr-2\">🏃</span>\n                <div>\n                  <span className=\"font-medium\">Activity Recognition</span>\n                  <p className=\"text-sm text-gray-600\">\n                    For automatic workout detection\n                  </p>\n                </div>\n              </li>\n              <li className=\"flex items-start\">\n                <span className=\"text-green-500 mr-2\">💾</span>\n                <div>\n                  <span className=\"font-medium\">Storage</span>\n                  <p className=\"text-sm text-gray-600\">\n                    For saving workout data and photos\n                  </p>\n                </div>\n              </li>\n            </ul>\n          </div>\n        </section>\n\n        <section>\n          <h2 className=\"text-2xl font-semibold mb-4\">Requirements</h2>\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <h3 className=\"font-semibold mb-3 flex items-center\">\n                <svg\n                  className=\"w-5 h-5 mr-2\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"currentColor\"\n                >\n                  <path d=\"M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z\" />\n                </svg>\n                iOS Requirements\n              </h3>\n              <ul className=\"text-gray-700 space-y-1 text-sm\">\n                <li>• iOS 13.0 or later</li>\n                <li>• Compatible with iPhone, iPad, and iPod touch</li>\n                <li>• Apple Watch app included</li>\n                <li>• Apple Health integration</li>\n              </ul>\n            </div>\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <h3 className=\"font-semibold mb-3 flex items-center\">\n                <svg\n                  className=\"w-5 h-5 mr-2\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"#3DDC84\"\n                >\n                  <path d=\"M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-3.198l2.807 1.626a1 1 0 0 1 0 1.73l-2.808 1.626L15.206 12l2.492-2.491zM5.864 2.658L16.802 8.99l-2.303 2.303-8.635-8.635z\" />\n                </svg>\n                Android Requirements\n              </h3>\n              <ul className=\"text-gray-700 space-y-1 text-sm\">\n                <li>• Android 7.0 (API level 24) or higher</li>\n                <li>• Works on phones and tablets</li>\n                <li>• Wear OS companion app</li>\n                <li>• Google Fit integration</li>\n              </ul>\n            </div>\n          </div>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/movie-carousel/page.tsx",
    "content": "import { MovieCarouselJsonLd } from \"next-seo\";\n\nexport default function MovieCarouselPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <MovieCarouselJsonLd\n        movies={[\n          {\n            name: \"A Star Is Born\",\n            image: \"https://example.com/photos/6x9/star-is-born.jpg\",\n            dateCreated: \"2024-10-05\",\n            director: \"Bradley Cooper\",\n            review: {\n              reviewRating: {\n                ratingValue: 5,\n              },\n              author: \"John D.\",\n            },\n            aggregateRating: {\n              ratingValue: 90,\n              bestRating: 100,\n              ratingCount: 19141,\n            },\n          },\n          {\n            name: \"Bohemian Rhapsody\",\n            image: \"https://example.com/photos/6x9/bohemian-rhapsody.jpg\",\n            dateCreated: \"2024-11-02\",\n            director: \"Bryan Singer\",\n            review: {\n              reviewRating: {\n                ratingValue: 3,\n              },\n              author: \"Vin S.\",\n            },\n            aggregateRating: {\n              ratingValue: 61,\n              bestRating: 100,\n              ratingCount: 21985,\n            },\n          },\n          {\n            name: \"Black Panther\",\n            image: \"https://example.com/photos/6x9/black-panther.jpg\",\n            dateCreated: \"2024-02-16\",\n            director: \"Ryan Coogler\",\n            review: {\n              reviewRating: {\n                ratingValue: 2,\n              },\n              author: \"Trevor R.\",\n            },\n            aggregateRating: {\n              ratingValue: 96,\n              bestRating: 100,\n              ratingCount: 88211,\n            },\n          },\n        ]}\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <h1>Movie Carousel - All-in-One Page Pattern</h1>\n        <p>\n          This page demonstrates the all-in-one page pattern for movie\n          carousels, where all movie information is contained on this single\n          page.\n        </p>\n\n        <h2>Best Picture Nominees 2024</h2>\n\n        <div className=\"space-y-8\">\n          <div id=\"a-star-is-born\" className=\"border-b pb-6\">\n            <h3>A Star Is Born</h3>\n            <p>Release Date: October 5, 2024</p>\n            <p>Director: Bradley Cooper</p>\n            <p>Rating: 90/100 (19,141 reviews)</p>\n            <p>\n              A musician helps a young singer find fame as age and alcoholism\n              send his own career into a downward spiral.\n            </p>\n          </div>\n\n          <div id=\"bohemian-rhapsody\" className=\"border-b pb-6\">\n            <h3>Bohemian Rhapsody</h3>\n            <p>Release Date: November 2, 2024</p>\n            <p>Director: Bryan Singer</p>\n            <p>Rating: 61/100 (21,985 reviews)</p>\n            <p>\n              The story of the legendary British rock band Queen and lead singer\n              Freddie Mercury, leading up to their famous performance at Live\n              Aid.\n            </p>\n          </div>\n\n          <div id=\"black-panther\" className=\"border-b pb-6\">\n            <h3>Black Panther</h3>\n            <p>Release Date: February 16, 2024</p>\n            <p>Director: Ryan Coogler</p>\n            <p>Rating: 96/100 (88,211 reviews)</p>\n            <p>\n              T'Challa, heir to the hidden but advanced kingdom of Wakanda, must\n              step forward to lead his people into a new future.\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/movie-carousel-advanced/page.tsx",
    "content": "import { MovieCarouselJsonLd } from \"next-seo\";\n\nexport default function MovieCarouselAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <MovieCarouselJsonLd\n        movies={[\n          {\n            name: \"Everything Everywhere All at Once\",\n            url: \"https://example.com/movies/everything-everywhere\",\n            image: [\n              \"https://example.com/photos/1x1/eeaao.jpg\",\n              {\n                url: \"https://example.com/photos/4x3/eeaao.jpg\",\n                width: 1200,\n                height: 900,\n              },\n              {\n                url: \"https://example.com/photos/16x9/eeaao.jpg\",\n                width: 1920,\n                height: 1080,\n              },\n              {\n                url: \"https://example.com/photos/6x9/eeaao.jpg\",\n                width: 600,\n                height: 900,\n                caption: \"Official movie poster\",\n              },\n            ],\n            dateCreated: \"2024-03-25\",\n            director: {\n              name: \"Daniel Kwan and Daniel Scheinert\",\n              url: \"https://example.com/directors/daniels\",\n            },\n            review: {\n              reviewRating: {\n                ratingValue: 5,\n                bestRating: 5,\n                worstRating: 1,\n              },\n              author: {\n                name: \"Sarah Johnson\",\n                url: \"https://example.com/reviewers/sarah-johnson\",\n              },\n              reviewBody:\n                \"A mind-bending masterpiece that explores the multiverse with heart, humor, and incredible creativity. The performances are outstanding.\",\n              datePublished: \"2024-03-30\",\n            },\n            aggregateRating: {\n              ratingValue: 95,\n              bestRating: 100,\n              worstRating: 0,\n              ratingCount: 125432,\n              reviewCount: 8956,\n            },\n          },\n          {\n            name: \"The Banshees of Inisherin\",\n            url: \"https://example.com/movies/banshees-inisherin\",\n            image: \"https://example.com/photos/6x9/banshees.jpg\",\n            dateCreated: \"2024-10-21\",\n            director: {\n              name: \"Martin McDonagh\",\n              url: \"https://example.com/directors/martin-mcdonagh\",\n              familyName: \"McDonagh\",\n              givenName: \"Martin\",\n            },\n            review: {\n              reviewRating: {\n                ratingValue: 4.5,\n                bestRating: 5,\n              },\n              author: \"Michael Chen\",\n              reviewBody:\n                \"A darkly comic tale of friendship's end on a remote Irish island. Colin Farrell and Brendan Gleeson deliver career-best performances.\",\n              datePublished: \"2024-10-25\",\n            },\n            aggregateRating: {\n              ratingValue: 87,\n              bestRating: 100,\n              ratingCount: 45678,\n            },\n          },\n          {\n            name: \"Top Gun: Maverick & Special IMAX Edition\",\n            url: \"https://example.com/movies/top-gun-maverick\",\n            image: [\n              \"https://example.com/photos/6x9/top-gun-poster.jpg\",\n              \"https://example.com/photos/16x9/top-gun-hero.jpg\",\n            ],\n            dateCreated: \"2024-05-27\",\n            director: \"Joseph Kosinski\",\n            review: {\n              reviewRating: {\n                ratingValue: 4,\n              },\n              author: {\n                name: \"Alex Rivera\",\n              },\n            },\n            aggregateRating: {\n              ratingValue: 92,\n              bestRating: 100,\n              ratingCount: 234567,\n            },\n          },\n        ]}\n        scriptId=\"movie-carousel-advanced\"\n        scriptKey=\"movie-carousel-key-advanced\"\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <h1>Movie Carousel - Advanced Features</h1>\n        <p>\n          This page demonstrates all available features of the\n          MovieCarouselJsonLd component, including:\n        </p>\n        <ul>\n          <li>Multiple image formats with ImageObject details</li>\n          <li>Complete director information with Person properties</li>\n          <li>Full review data with ratings and review body</li>\n          <li>Comprehensive aggregate ratings</li>\n          <li>Custom scriptId and scriptKey</li>\n          <li>Special characters in titles</li>\n        </ul>\n\n        <h2>Featured Movies</h2>\n\n        <div className=\"space-y-8\">\n          <div id=\"everything-everywhere\" className=\"border-b pb-6\">\n            <h3>Everything Everywhere All at Once</h3>\n            <p className=\"text-gray-600\">Released: March 25, 2024</p>\n            <p>Directors: Daniel Kwan and Daniel Scheinert</p>\n            <p>\n              <strong>Rating:</strong> 95/100 (125,432 ratings, 8,956 reviews)\n            </p>\n            <blockquote className=\"italic border-l-4 pl-4\">\n              \"A mind-bending masterpiece that explores the multiverse with\n              heart, humor, and incredible creativity. The performances are\n              outstanding.\"\n              <footer className=\"text-sm text-gray-600\">\n                — Sarah Johnson, March 30, 2024\n              </footer>\n            </blockquote>\n          </div>\n\n          <div id=\"banshees-inisherin\" className=\"border-b pb-6\">\n            <h3>The Banshees of Inisherin</h3>\n            <p className=\"text-gray-600\">Released: October 21, 2024</p>\n            <p>Director: Martin McDonagh</p>\n            <p>\n              <strong>Rating:</strong> 87/100 (45,678 ratings)\n            </p>\n            <blockquote className=\"italic border-l-4 pl-4\">\n              \"A darkly comic tale of friendship's end on a remote Irish island.\n              Colin Farrell and Brendan Gleeson deliver career-best\n              performances.\"\n              <footer className=\"text-sm text-gray-600\">\n                — Michael Chen, October 25, 2024\n              </footer>\n            </blockquote>\n          </div>\n\n          <div id=\"top-gun-maverick\" className=\"border-b pb-6\">\n            <h3>Top Gun: Maverick & Special IMAX Edition</h3>\n            <p className=\"text-gray-600\">Released: May 27, 2024</p>\n            <p>Director: Joseph Kosinski</p>\n            <p>\n              <strong>Rating:</strong> 92/100 (234,567 ratings)\n            </p>\n            <p className=\"italic\">\n              After more than 30 years of service, Pete \"Maverick\" Mitchell\n              continues to push the envelope as a top naval aviator.\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/movie-carousel-summary/page.tsx",
    "content": "import { MovieCarouselJsonLd } from \"next-seo\";\n\nexport default function MovieCarouselSummaryPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <MovieCarouselJsonLd\n        urls={[\n          \"https://example.com/movies/a-star-is-born\",\n          \"https://example.com/movies/bohemian-rhapsody\",\n          \"https://example.com/movies/black-panther\",\n          \"https://example.com/movies/green-book\",\n          \"https://example.com/movies/the-favourite\",\n        ]}\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <h1>Movie Carousel - Summary Page Pattern</h1>\n        <p>\n          This page demonstrates the summary page pattern for movie carousels,\n          where each movie has its own detail page.\n        </p>\n\n        <h2>Best Picture Nominees 2024</h2>\n        <ul>\n          <li>\n            <a href=\"https://example.com/movies/a-star-is-born\">\n              A Star Is Born\n            </a>\n          </li>\n          <li>\n            <a href=\"https://example.com/movies/bohemian-rhapsody\">\n              Bohemian Rhapsody\n            </a>\n          </li>\n          <li>\n            <a href=\"https://example.com/movies/black-panther\">Black Panther</a>\n          </li>\n          <li>\n            <a href=\"https://example.com/movies/green-book\">Green Book</a>\n          </li>\n          <li>\n            <a href=\"https://example.com/movies/the-favourite\">The Favourite</a>\n          </li>\n        </ul>\n\n        <p>\n          In this pattern, each movie URL points to a separate page with full\n          details about that movie.\n        </p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/news-article/page.tsx",
    "content": "import { ArticleJsonLd } from \"next-seo\";\n\nexport default function NewsArticlePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ArticleJsonLd\n        type=\"NewsArticle\"\n        headline=\"Breaking: Next.js 14 Released with Major Performance Improvements\"\n        url=\"https://example.com/news/nextjs-14-release\"\n        datePublished=\"2024-01-15T10:00:00+00:00\"\n        dateModified=\"2024-01-15T14:30:00+00:00\"\n        author={[\n          {\n            \"@type\": \"Person\",\n            name: \"Alex Chen\",\n            url: \"https://example.com/authors/alex-chen\",\n          },\n          {\n            \"@type\": \"Person\",\n            name: \"Maria Garcia\",\n            url: \"https://example.com/authors/maria-garcia\",\n          },\n        ]}\n        image={[\n          \"https://example.com/images/nextjs-14-16x9.jpg\",\n          \"https://example.com/images/nextjs-14-4x3.jpg\",\n          \"https://example.com/images/nextjs-14-1x1.jpg\",\n        ]}\n        publisher={{\n          \"@type\": \"Organization\",\n          name: \"Tech News Daily\",\n          logo: {\n            \"@type\": \"ImageObject\",\n            url: \"https://example.com/logo.png\",\n            width: 600,\n            height: 60,\n          },\n        }}\n        description=\"Next.js 14 brings significant performance improvements and new features\"\n        isAccessibleForFree={true}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <div className=\"bg-red-600 text-white px-4 py-2 rounded-t-lg inline-block\">\n          BREAKING NEWS\n        </div>\n\n        <h1>Next.js 14 Released with Major Performance Improvements</h1>\n\n        <div className=\"text-gray-600 mb-4\">\n          <p>By Alex Chen and Maria Garcia</p>\n          <p>Published: January 15, 2024 at 10:00 AM</p>\n          <p>Updated: January 15, 2024 at 2:30 PM</p>\n        </div>\n\n        <p className=\"lead text-xl\">\n          Vercel today announced the release of Next.js 14, bringing significant\n          performance improvements and developer experience enhancements to the\n          popular React framework.\n        </p>\n\n        <h2>Key Improvements</h2>\n        <ul>\n          <li>Turbopack is now 5,000x faster than Webpack</li>\n          <li>Server Actions are now stable</li>\n          <li>Partial Prerendering (Preview)</li>\n        </ul>\n\n        <p>\n          The development team has focused on improving local development\n          performance, with Turbopack showing remarkable speed improvements...\n        </p>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/online-store/page.tsx",
    "content": "import { OrganizationJsonLd } from \"next-seo\";\n\nexport default function OnlineStorePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <OrganizationJsonLd\n        type=\"OnlineStore\"\n        name=\"Example Online Store\"\n        url=\"https://www.example.com\"\n        logo={{\n          \"@type\": \"ImageObject\",\n          url: \"https://www.example.com/assets/logo.png\",\n          width: 600,\n          height: 400,\n        }}\n        description=\"Your trusted online retailer for premium widgets and accessories\"\n        sameAs={[\n          \"https://example.net/profile/example12\",\n          \"https://example.org/@example34\",\n        ]}\n        address={{\n          \"@type\": \"PostalAddress\",\n          streetAddress: \"999 W Example St Suite 99\",\n          addressLocality: \"New York\",\n          addressRegion: \"NY\",\n          postalCode: \"10019\",\n          addressCountry: \"US\",\n        }}\n        contactPoint={{\n          \"@type\": \"ContactPoint\",\n          contactType: \"Customer Service\",\n          telephone: \"+1-999-999-9900\",\n          email: \"support@example.com\",\n        }}\n        vatID=\"FR12345678901\"\n        iso6523Code=\"0199:724500PMK2A2M1SQQ228\"\n        numberOfEmployees={{\n          minValue: 100,\n          maxValue: 999,\n        }}\n        hasMerchantReturnPolicy={{\n          \"@type\": \"MerchantReturnPolicy\",\n          applicableCountry: [\"US\", \"CA\"],\n          returnPolicyCountry: \"US\",\n          returnPolicyCategory:\n            \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n          merchantReturnDays: 60,\n          itemCondition: [\"https://schema.org/NewCondition\"],\n          returnMethod: [\n            \"https://schema.org/ReturnByMail\",\n            \"https://schema.org/ReturnInStore\",\n          ],\n          returnFees: \"https://schema.org/FreeReturn\",\n          refundType: \"https://schema.org/FullRefund\",\n          returnLabelSource: \"https://schema.org/ReturnLabelDownloadAndPrint\",\n          customerRemorseReturnFees: \"https://schema.org/FreeReturn\",\n          itemDefectReturnFees: \"https://schema.org/FreeReturn\",\n          returnPolicySeasonalOverride: [\n            {\n              startDate: \"2025-11-29\",\n              endDate: \"2025-12-31\",\n              returnPolicyCategory:\n                \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n              merchantReturnDays: 90,\n            },\n          ],\n        }}\n        hasMemberProgram={{\n          name: \"Rewards Plus\",\n          description:\n            \"Earn points and unlock exclusive benefits with our loyalty program\",\n          url: \"https://www.example.com/rewards\",\n          hasTiers: [\n            {\n              name: \"Bronze\",\n              hasTierBenefit: \"TierBenefitLoyaltyPoints\",\n              membershipPointsEarned: 1,\n            },\n            {\n              name: \"Silver\",\n              hasTierBenefit: [\"TierBenefitLoyaltyPoints\"],\n              hasTierRequirement: {\n                value: 500,\n                currency: \"USD\",\n              },\n              membershipPointsEarned: 2,\n            },\n            {\n              name: \"Gold\",\n              hasTierBenefit: [\n                \"TierBenefitLoyaltyPoints\",\n                \"TierBenefitLoyaltyPrice\",\n              ],\n              hasTierRequirement: {\n                name: \"Example Gold Credit Card\",\n              },\n              membershipPointsEarned: 5,\n              url: \"https://www.example.com/rewards/gold\",\n            },\n          ],\n        }}\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-6\">Example Online Store</h1>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">About Us</h2>\n          <p className=\"text-gray-700 mb-4\">\n            Example Online Store is your trusted online retailer for premium\n            widgets and accessories. We serve customers across the United States\n            and Canada with fast shipping and excellent customer service.\n          </p>\n        </section>\n\n        <section className=\"mb-8 bg-gray-50 p-6 rounded-lg\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Return Policy</h2>\n          <ul className=\"space-y-2 text-gray-700\">\n            <li>✓ 60-day return window (90 days during holidays!)</li>\n            <li>✓ Free returns by mail or in-store</li>\n            <li>✓ Full refund guaranteed</li>\n            <li>✓ Applicable in US and Canada</li>\n            <li>✓ Download and print return label</li>\n            <li>✓ Free returns for all reasons</li>\n          </ul>\n          <div className=\"mt-4 p-3 bg-green-100 rounded\">\n            <p className=\"text-sm font-medium text-green-800\">\n              🎄 Holiday Special: Extended 90-day returns from Nov 29 - Dec 31\n            </p>\n          </div>\n        </section>\n\n        <section className=\"mb-8 bg-blue-50 p-6 rounded-lg\">\n          <h2 className=\"text-2xl font-semibold mb-4\">\n            Rewards Plus Loyalty Program\n          </h2>\n          <p className=\"text-gray-700 mb-4\">\n            Earn points and unlock exclusive benefits with our loyalty program!\n          </p>\n          <div className=\"space-y-4\">\n            <div className=\"border-l-4 border-orange-400 pl-4\">\n              <h3 className=\"font-semibold text-orange-800\">🥉 Bronze Tier</h3>\n              <p className=\"text-sm text-gray-600\">\n                Join free and earn 1 point per dollar spent\n              </p>\n            </div>\n            <div className=\"border-l-4 border-gray-400 pl-4\">\n              <h3 className=\"font-semibold text-gray-700\">🥈 Silver Tier</h3>\n              <p className=\"text-sm text-gray-600\">\n                Spend $500+ to earn 2 points per dollar\n              </p>\n            </div>\n            <div className=\"border-l-4 border-yellow-400 pl-4\">\n              <h3 className=\"font-semibold text-yellow-700\">🥇 Gold Tier</h3>\n              <p className=\"text-sm text-gray-600\">\n                Sign up for our Gold Credit Card to earn 5 points per dollar +\n                member pricing\n              </p>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Customer Service</h2>\n          <p className=\"text-gray-700\">\n            <strong>Phone:</strong> +1-999-999-9900\n            <br />\n            <strong>Email:</strong> support@example.com\n            <br />\n            <strong>Hours:</strong> Monday-Friday, 9AM-5PM EST\n          </p>\n        </section>\n\n        <section>\n          <h2 className=\"text-2xl font-semibold mb-4\">Our Location</h2>\n          <address className=\"text-gray-700 not-italic\">\n            999 W Example St Suite 99\n            <br />\n            New York, NY 10019\n            <br />\n            United States\n          </address>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/online-store-loyalty/page.tsx",
    "content": "import { OrganizationJsonLd } from \"next-seo\";\n\nexport default function OnlineStoreLoyaltyPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <OrganizationJsonLd\n        type=\"OnlineStore\"\n        name=\"Premium Store\"\n        url=\"https://www.premiumstore.com\"\n        logo=\"https://www.premiumstore.com/logo.png\"\n        description=\"Premium retailer with comprehensive loyalty programs\"\n        sameAs={[\"https://twitter.com/premiumstore\"]}\n        address={{\n          streetAddress: \"123 Premium Ave\",\n          addressLocality: \"San Francisco\",\n          addressRegion: \"CA\",\n          postalCode: \"94102\",\n          addressCountry: \"US\",\n        }}\n        hasMemberProgram={[\n          {\n            name: \"Basic Rewards\",\n            description: \"Our standard loyalty program for all customers\",\n            url: \"https://www.premiumstore.com/basic-rewards\",\n            hasTiers: [\n              {\n                name: \"Free Member\",\n                hasTierBenefit: \"TierBenefitLoyaltyPoints\",\n                membershipPointsEarned: 1,\n              },\n              {\n                name: \"Plus Member\",\n                hasTierBenefit: [\"TierBenefitLoyaltyPoints\"],\n                hasTierRequirement: {\n                  price: 4.99,\n                  priceCurrency: \"USD\",\n                  billingDuration: 12,\n                  billingIncrement: 1,\n                  unitCode: \"MON\",\n                },\n                membershipPointsEarned: 3,\n                url: \"https://www.premiumstore.com/plus-member\",\n              },\n            ],\n          },\n          {\n            name: \"VIP Elite Program\",\n            description: \"Exclusive program for our most valued customers\",\n            url: \"https://www.premiumstore.com/vip-elite\",\n            hasTiers: [\n              {\n                \"@id\": \"#vip-silver\",\n                name: \"Silver VIP\",\n                hasTierBenefit: [\n                  \"TierBenefitLoyaltyPoints\",\n                  \"TierBenefitLoyaltyPrice\",\n                ],\n                hasTierRequirement: {\n                  value: 2500,\n                  currency: \"USD\",\n                },\n                membershipPointsEarned: {\n                  value: 10,\n                  unitText: \"points per dollar\",\n                },\n                url: \"https://www.premiumstore.com/vip-silver\",\n              },\n              {\n                \"@id\": \"#vip-gold\",\n                name: \"Gold VIP\",\n                hasTierBenefit: [\n                  \"https://schema.org/TierBenefitLoyaltyPoints\",\n                  \"https://schema.org/TierBenefitLoyaltyPrice\",\n                ],\n                hasTierRequirement: {\n                  name: \"Premium Store Platinum Card\",\n                },\n                membershipPointsEarned: {\n                  value: 20,\n                  minValue: 20,\n                  maxValue: 40,\n                  unitText: \"points per dollar (double on special events)\",\n                },\n                url: \"https://www.premiumstore.com/vip-gold\",\n              },\n              {\n                \"@id\": \"#vip-diamond\",\n                name: \"Diamond VIP\",\n                hasTierBenefit: [\n                  \"https://schema.org/TierBenefitLoyaltyPoints\",\n                  \"https://schema.org/TierBenefitLoyaltyPrice\",\n                ],\n                hasTierRequirement:\n                  \"By invitation only - must maintain $10,000+ annual spending and participate in community events\",\n                membershipPointsEarned: 50,\n                url: \"https://www.premiumstore.com/vip-diamond\",\n              },\n            ],\n          },\n        ]}\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-6\">\n          Premium Store - Loyalty Programs\n        </h1>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">\n            Welcome to Premium Store\n          </h2>\n          <p className=\"text-gray-700 mb-4\">\n            We offer multiple loyalty programs designed to reward our valued\n            customers. Choose the program that best fits your shopping habits!\n          </p>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-3xl font-semibold mb-6\">Our Loyalty Programs</h2>\n\n          <div className=\"mb-8 bg-gray-50 p-6 rounded-lg\">\n            <h3 className=\"text-2xl font-semibold mb-4 text-blue-800\">\n              Basic Rewards\n            </h3>\n            <p className=\"text-gray-700 mb-4\">\n              Perfect for casual shoppers who want to earn points on every\n              purchase.\n            </p>\n\n            <div className=\"grid md:grid-cols-2 gap-4\">\n              <div className=\"bg-white p-4 rounded shadow\">\n                <h4 className=\"font-semibold text-lg mb-2\">🆓 Free Member</h4>\n                <ul className=\"text-sm text-gray-600 space-y-1\">\n                  <li>✓ No membership fee</li>\n                  <li>✓ Earn 1 point per dollar spent</li>\n                  <li>✓ Birthday bonus points</li>\n                </ul>\n              </div>\n\n              <div className=\"bg-white p-4 rounded shadow\">\n                <h4 className=\"font-semibold text-lg mb-2\">➕ Plus Member</h4>\n                <ul className=\"text-sm text-gray-600 space-y-1\">\n                  <li>✓ $4.99/month subscription</li>\n                  <li>✓ Earn 3 points per dollar spent</li>\n                  <li>✓ Free shipping on all orders</li>\n                  <li>✓ Early access to sales</li>\n                </ul>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"bg-gradient-to-r from-purple-50 to-pink-50 p-6 rounded-lg\">\n            <h3 className=\"text-2xl font-semibold mb-4 text-purple-800\">\n              VIP Elite Program\n            </h3>\n            <p className=\"text-gray-700 mb-4\">\n              Exclusive benefits for our most loyal customers with tiered\n              rewards.\n            </p>\n\n            <div className=\"space-y-4\">\n              <div className=\"bg-white p-4 rounded shadow\">\n                <h4 className=\"font-semibold text-lg mb-2\">🥈 Silver VIP</h4>\n                <p className=\"text-sm font-medium text-gray-700 mb-2\">\n                  Requirement: $2,500+ annual spending\n                </p>\n                <ul className=\"text-sm text-gray-600 space-y-1\">\n                  <li>✓ 10 points per dollar spent</li>\n                  <li>✓ Member-only pricing</li>\n                  <li>✓ Dedicated customer service line</li>\n                </ul>\n              </div>\n\n              <div className=\"bg-white p-4 rounded shadow\">\n                <h4 className=\"font-semibold text-lg mb-2\">🥇 Gold VIP</h4>\n                <p className=\"text-sm font-medium text-gray-700 mb-2\">\n                  Requirement: Premium Store Platinum Card holder\n                </p>\n                <ul className=\"text-sm text-gray-600 space-y-1\">\n                  <li>✓ 20-40 points per dollar (double on special events)</li>\n                  <li>✓ Exclusive member pricing</li>\n                  <li>✓ Personal shopping assistant</li>\n                  <li>✓ VIP event invitations</li>\n                </ul>\n              </div>\n\n              <div className=\"bg-gradient-to-r from-gray-100 to-gray-200 p-4 rounded shadow\">\n                <h4 className=\"font-semibold text-lg mb-2\">💎 Diamond VIP</h4>\n                <p className=\"text-sm font-medium text-gray-700 mb-2\">\n                  Requirement: By invitation only\n                </p>\n                <ul className=\"text-sm text-gray-600 space-y-1\">\n                  <li>✓ 50 points per dollar spent</li>\n                  <li>✓ Best possible pricing</li>\n                  <li>✓ White glove service</li>\n                  <li>✓ Exclusive product launches</li>\n                  <li>✓ Annual appreciation gifts</li>\n                </ul>\n                <p className=\"text-xs text-gray-500 mt-2 italic\">\n                  Must maintain $10,000+ annual spending and participate in\n                  community events\n                </p>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-8 bg-green-50 p-6 rounded-lg\">\n          <h2 className=\"text-2xl font-semibold mb-4\">How Points Work</h2>\n          <div className=\"grid md:grid-cols-2 gap-4\">\n            <div>\n              <h3 className=\"font-semibold mb-2\">Earning Points</h3>\n              <ul className=\"text-sm text-gray-700 space-y-1\">\n                <li>• Shop in-store or online</li>\n                <li>• Write product reviews</li>\n                <li>• Refer friends</li>\n                <li>• Special promotions</li>\n              </ul>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-2\">Redeeming Points</h3>\n              <ul className=\"text-sm text-gray-700 space-y-1\">\n                <li>• 100 points = $1 off</li>\n                <li>• Exclusive products</li>\n                <li>• Gift cards</li>\n                <li>• Charitable donations</li>\n              </ul>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"text-center\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Join Today!</h2>\n          <p className=\"text-gray-700 mb-4\">\n            Start earning rewards with your next purchase.\n          </p>\n          <button className=\"bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-700\">\n            Sign Up for Rewards\n          </button>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/organization/page.tsx",
    "content": "import { OrganizationJsonLd } from \"next-seo\";\n\nexport default function OrganizationPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <OrganizationJsonLd\n        name=\"Example Corporation\"\n        url=\"https://www.example.com\"\n        logo=\"https://www.example.com/logo.png\"\n        description=\"The example corporation is well-known for producing high-quality widgets\"\n        sameAs={[\n          \"https://twitter.com/example\",\n          \"https://facebook.com/example\",\n          \"https://linkedin.com/company/example\",\n        ]}\n        telephone=\"+1-999-999-9999\"\n        email=\"contact@example.com\"\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-6\">About Example Corporation</h1>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Our Mission</h2>\n          <p className=\"text-gray-700 mb-4\">\n            Example Corporation is dedicated to producing the highest quality\n            widgets in the industry. Since our founding, we have been committed\n            to innovation, sustainability, and customer satisfaction.\n          </p>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Contact Us</h2>\n          <p className=\"text-gray-700\">\n            <strong>Phone:</strong> +1-999-999-9999\n            <br />\n            <strong>Email:</strong> contact@example.com\n          </p>\n        </section>\n\n        <section>\n          <h2 className=\"text-2xl font-semibold mb-4\">Connect With Us</h2>\n          <ul className=\"space-y-2\">\n            <li>\n              <a\n                href=\"https://twitter.com/example\"\n                className=\"text-blue-600 hover:underline\"\n              >\n                Twitter\n              </a>\n            </li>\n            <li>\n              <a\n                href=\"https://facebook.com/example\"\n                className=\"text-blue-600 hover:underline\"\n              >\n                Facebook\n              </a>\n            </li>\n            <li>\n              <a\n                href=\"https://linkedin.com/company/example\"\n                className=\"text-blue-600 hover:underline\"\n              >\n                LinkedIn\n              </a>\n            </li>\n          </ul>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/organization-advanced/page.tsx",
    "content": "import { OrganizationJsonLd } from \"next-seo\";\n\nexport default function OrganizationAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <OrganizationJsonLd\n        name=\"Global Widget Corporation\"\n        url=\"https://www.globalwidget.com\"\n        logo={{\n          \"@type\": \"ImageObject\",\n          url: \"https://www.globalwidget.com/logo.png\",\n          width: 800,\n          height: 600,\n          caption: \"Global Widget Corporation Logo\",\n        }}\n        description=\"A multinational corporation specializing in innovative widget solutions with offices worldwide\"\n        sameAs={[\n          \"https://twitter.com/globalwidget\",\n          \"https://facebook.com/globalwidget\",\n          \"https://linkedin.com/company/global-widget-corp\",\n          \"https://instagram.com/globalwidget\",\n        ]}\n        address={[\n          {\n            \"@type\": \"PostalAddress\",\n            streetAddress: \"123 Tech Plaza, Suite 1000\",\n            addressLocality: \"San Francisco\",\n            addressRegion: \"CA\",\n            postalCode: \"94105\",\n            addressCountry: \"US\",\n          },\n          {\n            \"@type\": \"PostalAddress\",\n            streetAddress: \"456 Innovation Drive\",\n            addressLocality: \"London\",\n            addressRegion: \"England\",\n            postalCode: \"EC2A 4BX\",\n            addressCountry: \"GB\",\n          },\n          {\n            \"@type\": \"PostalAddress\",\n            streetAddress: \"789 Business Center\",\n            addressLocality: \"Tokyo\",\n            addressRegion: \"Tokyo\",\n            postalCode: \"100-0001\",\n            addressCountry: \"JP\",\n          },\n        ]}\n        contactPoint={[\n          {\n            \"@type\": \"ContactPoint\",\n            contactType: \"Customer Service\",\n            telephone: \"+1-800-123-4567\",\n            email: \"support@globalwidget.com\",\n          },\n          {\n            \"@type\": \"ContactPoint\",\n            contactType: \"Sales\",\n            telephone: \"+1-800-123-4568\",\n            email: \"sales@globalwidget.com\",\n          },\n          {\n            \"@type\": \"ContactPoint\",\n            contactType: \"Technical Support\",\n            telephone: \"+1-800-123-4569\",\n            email: \"tech@globalwidget.com\",\n          },\n        ]}\n        telephone=\"+1-800-123-4567\"\n        email=\"info@globalwidget.com\"\n        alternateName=\"GWC\"\n        foundingDate=\"1995-03-15\"\n        legalName=\"Global Widget Corporation Inc.\"\n        taxID=\"98-7654321\"\n        vatID=\"GB123456789\"\n        duns=\"123456789\"\n        leiCode=\"529900T8BM49AURSDO55\"\n        naics=\"334111\"\n        globalLocationNumber=\"0614141000001\"\n        iso6523Code=\"0088:0614141000001\"\n        numberOfEmployees={{\n          minValue: 5000,\n          maxValue: 10000,\n        }}\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-6\">Global Widget Corporation</h1>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">About GWC</h2>\n          <p className=\"text-gray-700 mb-4\">\n            Founded in 1995, Global Widget Corporation (GWC) has grown from a\n            small startup to a multinational corporation with over 5,000\n            employees worldwide. We specialize in innovative widget solutions\n            that power businesses across the globe.\n          </p>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Our Global Offices</h2>\n          <div className=\"grid md:grid-cols-3 gap-6\">\n            <div className=\"bg-gray-50 p-4 rounded-lg\">\n              <h3 className=\"font-semibold mb-2\">Americas Headquarters</h3>\n              <address className=\"text-gray-700 text-sm not-italic\">\n                123 Tech Plaza, Suite 1000\n                <br />\n                San Francisco, CA 94105\n                <br />\n                United States\n              </address>\n            </div>\n            <div className=\"bg-gray-50 p-4 rounded-lg\">\n              <h3 className=\"font-semibold mb-2\">EMEA Office</h3>\n              <address className=\"text-gray-700 text-sm not-italic\">\n                456 Innovation Drive\n                <br />\n                London EC2A 4BX\n                <br />\n                United Kingdom\n              </address>\n            </div>\n            <div className=\"bg-gray-50 p-4 rounded-lg\">\n              <h3 className=\"font-semibold mb-2\">APAC Office</h3>\n              <address className=\"text-gray-700 text-sm not-italic\">\n                789 Business Center\n                <br />\n                Tokyo 100-0001\n                <br />\n                Japan\n              </address>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Contact Departments</h2>\n          <div className=\"space-y-4\">\n            <div>\n              <h3 className=\"font-semibold\">Customer Service</h3>\n              <p className=\"text-gray-700\">\n                Phone: +1-800-123-4567 | Email: support@globalwidget.com\n              </p>\n            </div>\n            <div>\n              <h3 className=\"font-semibold\">Sales</h3>\n              <p className=\"text-gray-700\">\n                Phone: +1-800-123-4568 | Email: sales@globalwidget.com\n              </p>\n            </div>\n            <div>\n              <h3 className=\"font-semibold\">Technical Support</h3>\n              <p className=\"text-gray-700\">\n                Phone: +1-800-123-4569 | Email: tech@globalwidget.com\n              </p>\n            </div>\n          </div>\n        </section>\n\n        <section>\n          <h2 className=\"text-2xl font-semibold mb-4\">Corporate Information</h2>\n          <dl className=\"grid grid-cols-2 gap-4 text-sm\">\n            <div>\n              <dt className=\"font-semibold text-gray-600\">Founded</dt>\n              <dd className=\"text-gray-700\">March 15, 1995</dd>\n            </div>\n            <div>\n              <dt className=\"font-semibold text-gray-600\">Legal Name</dt>\n              <dd className=\"text-gray-700\">Global Widget Corporation Inc.</dd>\n            </div>\n            <div>\n              <dt className=\"font-semibold text-gray-600\">Employees</dt>\n              <dd className=\"text-gray-700\">5,000 - 10,000</dd>\n            </div>\n            <div>\n              <dt className=\"font-semibold text-gray-600\">Industry Code</dt>\n              <dd className=\"text-gray-700\">NAICS 334111</dd>\n            </div>\n          </dl>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/organization-reviews/page.tsx",
    "content": "import { OrganizationJsonLd } from \"next-seo\";\n\nexport default function OrganizationReviewsPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <OrganizationJsonLd\n        name=\"Acme Software Inc.\"\n        url=\"https://www.acmesoftware.com\"\n        logo=\"https://www.acmesoftware.com/logo.png\"\n        description=\"Acme Software delivers enterprise-grade solutions for businesses worldwide\"\n        sameAs={[\n          \"https://twitter.com/acmesoftware\",\n          \"https://linkedin.com/company/acme-software\",\n        ]}\n        telephone=\"+1-800-555-0199\"\n        email=\"info@acmesoftware.com\"\n        review={[\n          {\n            author: \"Sarah Johnson\",\n            reviewBody:\n              \"Acme Software transformed our business operations. Their enterprise platform is reliable and their support team is outstanding.\",\n            reviewRating: {\n              ratingValue: 5,\n              bestRating: 5,\n            },\n            datePublished: \"2025-06-15\",\n          },\n          {\n            author: {\n              name: \"Michael Chen\",\n              url: \"https://example.com/michael-chen\",\n            },\n            reviewBody:\n              \"Great software solutions with excellent customer service. Highly recommend for mid-size businesses.\",\n            reviewRating: {\n              ratingValue: 4,\n              bestRating: 5,\n            },\n            datePublished: \"2025-08-22\",\n          },\n        ]}\n        aggregateRating={{\n          ratingValue: 4.6,\n          ratingCount: 312,\n          reviewCount: 245,\n          bestRating: 5,\n          worstRating: 1,\n        }}\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <h1 className=\"text-4xl font-bold mb-6\">Acme Software Inc.</h1>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">About Us</h2>\n          <p className=\"text-gray-700 mb-4\">\n            Acme Software delivers enterprise-grade solutions for businesses\n            worldwide. With over 300 verified reviews and an average rating of\n            4.6 out of 5, we are trusted by thousands of organizations.\n          </p>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Customer Reviews</h2>\n          <div className=\"space-y-4\">\n            <div className=\"bg-gray-50 p-4 rounded-lg\">\n              <div className=\"flex items-center mb-2\">\n                <span className=\"font-semibold\">Sarah Johnson</span>\n                <span className=\"ml-2 text-yellow-500\">5/5</span>\n              </div>\n              <p className=\"text-gray-700\">\n                Acme Software transformed our business operations. Their\n                enterprise platform is reliable and their support team is\n                outstanding.\n              </p>\n            </div>\n            <div className=\"bg-gray-50 p-4 rounded-lg\">\n              <div className=\"flex items-center mb-2\">\n                <span className=\"font-semibold\">Michael Chen</span>\n                <span className=\"ml-2 text-yellow-500\">4/5</span>\n              </div>\n              <p className=\"text-gray-700\">\n                Great software solutions with excellent customer service. Highly\n                recommend for mid-size businesses.\n              </p>\n            </div>\n          </div>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/page.module.css",
    "content": ".page {\n  --gray-rgb: 0, 0, 0;\n  --gray-alpha-200: rgba(var(--gray-rgb), 0.08);\n  --gray-alpha-100: rgba(var(--gray-rgb), 0.05);\n\n  --button-primary-hover: #383838;\n  --button-secondary-hover: #f2f2f2;\n\n  display: grid;\n  grid-template-rows: 20px 1fr 20px;\n  align-items: center;\n  justify-items: center;\n  min-height: 100svh;\n  padding: 80px;\n  gap: 64px;\n  font-family: var(--font-geist-sans);\n}\n\n@media (prefers-color-scheme: dark) {\n  .page {\n    --gray-rgb: 255, 255, 255;\n    --gray-alpha-200: rgba(var(--gray-rgb), 0.145);\n    --gray-alpha-100: rgba(var(--gray-rgb), 0.06);\n\n    --button-primary-hover: #ccc;\n    --button-secondary-hover: #1a1a1a;\n  }\n}\n\n.main {\n  display: flex;\n  flex-direction: column;\n  gap: 32px;\n  grid-row-start: 2;\n}\n\n.main ol {\n  font-family: var(--font-geist-mono);\n  padding-left: 0;\n  margin: 0;\n  font-size: 14px;\n  line-height: 24px;\n  letter-spacing: -0.01em;\n  list-style-position: inside;\n}\n\n.main li:not(:last-of-type) {\n  margin-bottom: 8px;\n}\n\n.main code {\n  font-family: inherit;\n  background: var(--gray-alpha-100);\n  padding: 2px 4px;\n  border-radius: 4px;\n  font-weight: 600;\n}\n\n.ctas {\n  display: flex;\n  gap: 16px;\n}\n\n.ctas a {\n  appearance: none;\n  border-radius: 128px;\n  height: 48px;\n  padding: 0 20px;\n  border: none;\n  border: 1px solid transparent;\n  transition:\n    background 0.2s,\n    color 0.2s,\n    border-color 0.2s;\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 16px;\n  line-height: 20px;\n  font-weight: 500;\n}\n\na.primary {\n  background: var(--foreground);\n  color: var(--background);\n  gap: 8px;\n}\n\na.secondary {\n  border-color: var(--gray-alpha-200);\n  min-width: 158px;\n}\n\n.footer {\n  grid-row-start: 3;\n  display: flex;\n  gap: 24px;\n}\n\n.footer a {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.footer img {\n  flex-shrink: 0;\n}\n\n/* Enable hover only on non-touch devices */\n@media (hover: hover) and (pointer: fine) {\n  a.primary:hover {\n    background: var(--button-primary-hover);\n    border-color: transparent;\n  }\n\n  a.secondary:hover {\n    background: var(--button-secondary-hover);\n    border-color: transparent;\n  }\n\n  .footer a:hover {\n    text-decoration: underline;\n    text-underline-offset: 4px;\n  }\n}\n\n@media (max-width: 600px) {\n  .page {\n    padding: 32px;\n    padding-bottom: 80px;\n  }\n\n  .main {\n    align-items: center;\n  }\n\n  .main ol {\n    text-align: center;\n  }\n\n  .ctas {\n    flex-direction: column;\n  }\n\n  .ctas a {\n    font-size: 14px;\n    height: 40px;\n    padding: 0 16px;\n  }\n\n  a.secondary {\n    min-width: auto;\n  }\n\n  .footer {\n    flex-wrap: wrap;\n    align-items: center;\n    justify-content: center;\n  }\n}\n\n@media (prefers-color-scheme: dark) {\n  .logo {\n    filter: invert();\n  }\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/page.tsx",
    "content": "import Image from \"next/image\";\nimport styles from \"./page.module.css\";\n\nexport default function Home() {\n  return (\n    <div className={styles.page}>\n      <main className={styles.main}>\n        <Image\n          className={styles.logo}\n          src=\"/next.svg\"\n          alt=\"Next.js logo\"\n          width={180}\n          height={38}\n          priority\n        />\n        <ol>\n          <li>\n            Get started by editing <code>src/app/page.tsx</code>.\n          </li>\n          <li>Save and see your changes instantly.</li>\n        </ol>\n\n        <div className={styles.ctas}>\n          <a\n            className={styles.primary}\n            href=\"https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            <Image\n              className={styles.logo}\n              src=\"/vercel.svg\"\n              alt=\"Vercel logomark\"\n              width={20}\n              height={20}\n            />\n            Deploy now\n          </a>\n          <a\n            href=\"https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className={styles.secondary}\n          >\n            Read our docs\n          </a>\n        </div>\n      </main>\n      <footer className={styles.footer}>\n        <a\n          href=\"https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          <Image\n            aria-hidden\n            src=\"/file.svg\"\n            alt=\"File icon\"\n            width={16}\n            height={16}\n          />\n          Learn\n        </a>\n        <a\n          href=\"https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          <Image\n            aria-hidden\n            src=\"/window.svg\"\n            alt=\"Window icon\"\n            width={16}\n            height={16}\n          />\n          Examples\n        </a>\n        <a\n          href=\"https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          <Image\n            aria-hidden\n            src=\"/globe.svg\"\n            alt=\"Globe icon\"\n            width={16}\n            height={16}\n          />\n          Go to nextjs.org →\n        </a>\n      </footer>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function ProductPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        description=\"Sleeker than ACME's Classic Anvil, the Executive Anvil is perfect for the business traveler looking for something to drop from a height.\"\n        url=\"https://example.com/products/anvil\"\n        image={[\n          \"https://example.com/photos/1x1/photo.jpg\",\n          \"https://example.com/photos/4x3/photo.jpg\",\n          \"https://example.com/photos/16x9/photo.jpg\",\n        ]}\n        sku=\"0446310786\"\n        mpn=\"925872\"\n        brand=\"ACME\"\n        offers={{\n          price: 119.99,\n          priceCurrency: \"USD\",\n          availability: \"InStock\",\n          priceValidUntil: \"2024-12-31\",\n          url: \"https://example.com/buy/anvil\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.4,\n          reviewCount: 89,\n        }}\n      />\n\n      <div className=\"max-w-4xl\">\n        <nav className=\"mb-8\">\n          <ol className=\"flex space-x-2 text-sm text-gray-600\">\n            <li>Home</li>\n            <li>/</li>\n            <li>Products</li>\n            <li>/</li>\n            <li className=\"font-semibold text-gray-900\">Executive Anvil</li>\n          </ol>\n        </nav>\n\n        <div className=\"grid md:grid-cols-2 gap-8\">\n          <div className=\"space-y-4\">\n            <div className=\"bg-gray-200 rounded-lg aspect-square flex items-center justify-center\">\n              <span className=\"text-gray-500\">Product Image</span>\n            </div>\n            <div className=\"grid grid-cols-3 gap-2\">\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n            </div>\n          </div>\n\n          <div className=\"space-y-6\">\n            <div>\n              <h1 className=\"text-3xl font-bold mb-2\">Executive Anvil</h1>\n              <div className=\"flex items-center gap-4 mb-4\">\n                <div className=\"flex text-yellow-400\">{\"★★★★☆\"}</div>\n                <span className=\"text-gray-600\">4.4 out of 5 (89 reviews)</span>\n              </div>\n              <p className=\"text-gray-600\">by ACME</p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <div className=\"flex items-baseline gap-2\">\n                <span className=\"text-3xl font-bold\">$119.99</span>\n                <span className=\"text-sm text-gray-500\">USD</span>\n              </div>\n              <p className=\"text-green-600 font-medium\">✓ In Stock</p>\n              <p className=\"text-sm text-gray-500\">\n                Price valid until Dec 31, 2024\n              </p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <p className=\"text-sm\">\n                <strong>SKU:</strong> 0446310786\n              </p>\n              <p className=\"text-sm\">\n                <strong>MPN:</strong> 925872\n              </p>\n            </div>\n\n            <div className=\"space-y-4\">\n              <p className=\"text-gray-700\">\n                Sleeker than ACME's Classic Anvil, the Executive Anvil is\n                perfect for the business traveler looking for something to drop\n                from a height. Crafted from the finest steel and polished to a\n                mirror finish, this anvil combines functionality with executive\n                style.\n              </p>\n            </div>\n\n            <div className=\"space-y-3\">\n              <button className=\"w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700\">\n                Add to Cart\n              </button>\n              <button className=\"w-full border border-gray-300 py-3 px-6 rounded-lg font-medium hover:bg-gray-50\">\n                Add to Wishlist\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Product Details</h2>\n          <div className=\"grid md:grid-cols-2 gap-8\">\n            <div>\n              <h3 className=\"font-semibold mb-3\">Features</h3>\n              <ul className=\"space-y-2 text-gray-700\">\n                <li>• Premium steel construction</li>\n                <li>• Mirror-polished finish</li>\n                <li>• Ergonomic design for dropping</li>\n                <li>• Executive briefcase included</li>\n                <li>• Lifetime warranty</li>\n              </ul>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Specifications</h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Weight:</dt>\n                  <dd>10 kg</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Dimensions:</dt>\n                  <dd>30cm x 20cm x 15cm</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Material:</dt>\n                  <dd>Steel</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Color:</dt>\n                  <dd>Silver</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Model:</dt>\n                  <dd>EA-2024</dd>\n                </div>\n              </dl>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product-3d-model/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function Product3DModelPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProductJsonLd\n        name=\"Modern Lounge Chair\"\n        description=\"Ergonomic lounge chair with premium leather upholstery and swivel base\"\n        url=\"https://example.com/products/modern-lounge-chair\"\n        image={[\n          \"https://example.com/chair-1x1.jpg\",\n          \"https://example.com/chair-4x3.jpg\",\n          \"https://example.com/chair-16x9.jpg\",\n        ]}\n        sku=\"CHAIR-ML-001\"\n        mpn=\"MLC2024\"\n        gtin14=\"00012345678912\"\n        brand=\"Designer Furniture Studio\"\n        // 3D model reference\n        subjectOf={{\n          encoding: {\n            contentUrl: \"https://example.com/models/lounge-chair.gltf\",\n          },\n        }}\n        // Target audience\n        audience={{\n          suggestedGender: \"unisex\",\n          suggestedMinAge: 18,\n          suggestedMaxAge: 65,\n        }}\n        offers={{\n          price: 1299.0,\n          priceCurrency: \"USD\",\n          availability: \"InStock\",\n          priceValidUntil: \"2024-12-31\",\n          itemCondition: \"https://schema.org/NewCondition\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.8,\n          reviewCount: 127,\n        }}\n        // Enhanced size specifications\n        size={{\n          name: \"Standard\",\n          sizeSystem: \"https://schema.org/WearableSizeSystemUS\",\n        }}\n        color=\"Cognac Leather\"\n        material=\"Italian Leather, Steel Frame\"\n        pattern=\"Solid\"\n      />\n\n      <div className=\"max-w-4xl\">\n        <nav className=\"mb-8\">\n          <ol className=\"flex space-x-2 text-sm text-gray-600\">\n            <li>Home</li>\n            <li>/</li>\n            <li>Furniture</li>\n            <li>/</li>\n            <li>Seating</li>\n            <li>/</li>\n            <li className=\"font-semibold text-gray-900\">Modern Lounge Chair</li>\n          </ol>\n        </nav>\n\n        <div className=\"grid md:grid-cols-2 gap-8\">\n          <div className=\"space-y-4\">\n            <div className=\"bg-gray-100 rounded-lg aspect-square flex flex-col items-center justify-center p-8\">\n              <div className=\"text-center\">\n                <span className=\"text-gray-500 block mb-4\">Chair 3D Model</span>\n                <button className=\"bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-2 mx-auto\">\n                  <svg\n                    className=\"w-5 h-5\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    viewBox=\"0 0 24 24\"\n                  >\n                    <path\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      strokeWidth={2}\n                      d=\"M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122\"\n                    />\n                  </svg>\n                  View in 3D\n                </button>\n              </div>\n            </div>\n            <div className=\"grid grid-cols-4 gap-2\">\n              <div className=\"bg-gray-100 rounded aspect-square\"></div>\n              <div className=\"bg-gray-100 rounded aspect-square\"></div>\n              <div className=\"bg-gray-100 rounded aspect-square\"></div>\n              <div className=\"bg-gray-100 rounded aspect-square flex items-center justify-center\">\n                <span className=\"text-xs text-gray-500\">360°</span>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"space-y-6\">\n            <div>\n              <h1 className=\"text-3xl font-bold mb-2\">Modern Lounge Chair</h1>\n              <div className=\"flex items-center gap-4 mb-4\">\n                <div className=\"flex text-yellow-400\">{\"★★★★★\"}</div>\n                <span className=\"text-gray-600\">\n                  4.8 out of 5 (127 reviews)\n                </span>\n              </div>\n              <p className=\"text-gray-600\">by Designer Furniture Studio</p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <div className=\"flex items-baseline gap-2\">\n                <span className=\"text-3xl font-bold\">$1,299.00</span>\n                <span className=\"text-sm text-gray-500\">USD</span>\n              </div>\n              <p className=\"text-green-600 font-medium\">✓ In Stock</p>\n              <div className=\"flex gap-2 text-sm\">\n                <span className=\"bg-gray-100 px-2 py-1 rounded\">\n                  New Condition\n                </span>\n                <span className=\"bg-blue-100 text-blue-700 px-2 py-1 rounded\">\n                  3D View Available\n                </span>\n              </div>\n            </div>\n\n            <div className=\"border rounded-lg p-4 bg-gradient-to-r from-blue-50 to-purple-50\">\n              <h3 className=\"font-semibold mb-2 flex items-center gap-2\">\n                <svg\n                  className=\"w-5 h-5 text-purple-600\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  viewBox=\"0 0 24 24\"\n                >\n                  <path\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    strokeWidth={2}\n                    d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n                  />\n                </svg>\n                Interactive 3D Model\n              </h3>\n              <p className=\"text-sm text-gray-700 mb-3\">\n                View this product in 3D! Rotate, zoom, and explore every detail\n                before you buy.\n              </p>\n              <button className=\"w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white py-2 px-4 rounded-lg hover:from-blue-700 hover:to-purple-700\">\n                Launch 3D Viewer\n              </button>\n            </div>\n\n            <div className=\"space-y-3\">\n              <div>\n                <h4 className=\"text-sm font-semibold mb-2\">Color Options</h4>\n                <div className=\"flex gap-2\">\n                  <button className=\"w-10 h-10 rounded-full bg-amber-700 border-2 border-gray-300 hover:border-gray-500\" />\n                  <button className=\"w-10 h-10 rounded-full bg-gray-800 border-2 border-gray-300 hover:border-gray-500\" />\n                  <button className=\"w-10 h-10 rounded-full bg-gray-300 border-2 border-gray-300 hover:border-gray-500\" />\n                  <button className=\"w-10 h-10 rounded-full bg-blue-900 border-2 border-gray-300 hover:border-gray-500\" />\n                </div>\n              </div>\n            </div>\n\n            <div className=\"bg-gray-50 rounded-lg p-3\">\n              <h4 className=\"text-sm font-semibold mb-2\">Target Audience</h4>\n              <div className=\"text-sm text-gray-600 space-y-1\">\n                <p>• Suitable for: Adults (18-65 years)</p>\n                <p>• Design: Unisex appeal</p>\n                <p>• Style: Modern, minimalist</p>\n              </div>\n            </div>\n\n            <div className=\"space-y-2\">\n              <p className=\"text-sm\">\n                <strong>SKU:</strong> CHAIR-ML-001\n              </p>\n              <p className=\"text-sm\">\n                <strong>MPN:</strong> MLC2024\n              </p>\n              <p className=\"text-sm\">\n                <strong>GTIN:</strong> 00012345678912\n              </p>\n              <p className=\"text-sm\">\n                <strong>Material:</strong> Italian Leather, Steel Frame\n              </p>\n              <p className=\"text-sm\">\n                <strong>Color:</strong> Cognac Leather\n              </p>\n            </div>\n\n            <div className=\"space-y-3\">\n              <button className=\"w-full bg-black text-white py-3 px-6 rounded-lg font-medium hover:bg-gray-800\">\n                Add to Cart\n              </button>\n              <button className=\"w-full border border-gray-300 py-3 px-6 rounded-lg font-medium hover:bg-gray-50\">\n                Request Fabric Samples\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Product Specifications</h2>\n          <div className=\"grid md:grid-cols-3 gap-8\">\n            <div>\n              <h3 className=\"font-semibold mb-3\">Dimensions</h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Width:</dt>\n                  <dd>30 inches</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Depth:</dt>\n                  <dd>32 inches</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Height:</dt>\n                  <dd>40 inches</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Seat Height:</dt>\n                  <dd>17 inches</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Weight:</dt>\n                  <dd>45 lbs</dd>\n                </div>\n              </dl>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Materials & Care</h3>\n              <ul className=\"space-y-2 text-sm text-gray-700\">\n                <li>• Top-grain Italian leather</li>\n                <li>• Powder-coated steel frame</li>\n                <li>• High-density foam cushioning</li>\n                <li>• 360° swivel base</li>\n                <li>• Clean with leather conditioner</li>\n              </ul>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Features</h3>\n              <ul className=\"space-y-2 text-sm text-gray-700\">\n                <li>• Ergonomic design</li>\n                <li>• Adjustable headrest</li>\n                <li>• Built-in lumbar support</li>\n                <li>• Smooth swivel mechanism</li>\n                <li>• 5-year warranty</li>\n              </ul>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-8 bg-purple-50 rounded-lg p-6\">\n          <h3 className=\"font-semibold mb-3 flex items-center gap-2\">\n            <span className=\"text-purple-600\">✨</span>\n            About Our 3D Models\n          </h3>\n          <p className=\"text-sm text-gray-700 mb-3\">\n            Our interactive 3D models are created using photogrammetry and\n            professional 3D scanning to provide the most accurate representation\n            of our products. View products from every angle, zoom in on details,\n            and see how they'll look in your space using AR technology.\n          </p>\n          <p className=\"text-sm text-gray-600\">\n            3D models are available in GLTF format and compatible with most\n            modern browsers and AR applications.\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product-aggregate/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function ProductAggregatePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        description=\"Sleeker than ACME's Classic Anvil, the Executive Anvil is perfect for the business traveler looking for something to drop from a height.\"\n        url=\"https://example.com/products/anvil\"\n        image={[\n          \"https://example.com/photos/1x1/photo.jpg\",\n          \"https://example.com/photos/4x3/photo.jpg\",\n          \"https://example.com/photos/16x9/photo.jpg\",\n        ]}\n        sku=\"0446310786\"\n        mpn=\"925872\"\n        brand=\"ACME\"\n        offers={{\n          lowPrice: 119.99,\n          highPrice: 199.99,\n          priceCurrency: \"USD\",\n          offerCount: 5,\n        }}\n        review={[\n          {\n            reviewRating: {\n              ratingValue: 5,\n              bestRating: 5,\n            },\n            author: \"Fred Benson\",\n            reviewBody:\n              \"This anvil is perfect! Exactly what I needed for my roadrunner traps.\",\n            datePublished: \"2024-01-10\",\n          },\n          {\n            reviewRating: {\n              ratingValue: 4,\n              bestRating: 5,\n            },\n            author: \"Wile E. Coyote\",\n            reviewBody: \"Great anvil, but shipping took longer than expected.\",\n            datePublished: \"2024-01-05\",\n          },\n        ]}\n        aggregateRating={{\n          ratingValue: 4.4,\n          reviewCount: 89,\n        }}\n      />\n\n      <div className=\"max-w-6xl\">\n        <nav className=\"mb-8\">\n          <ol className=\"flex space-x-2 text-sm text-gray-600\">\n            <li>Home</li>\n            <li>/</li>\n            <li>Compare</li>\n            <li>/</li>\n            <li className=\"font-semibold text-gray-900\">Executive Anvil</li>\n          </ol>\n        </nav>\n\n        <div className=\"grid lg:grid-cols-3 gap-8\">\n          <div className=\"lg:col-span-1\">\n            <div className=\"sticky top-8 space-y-6\">\n              <div className=\"bg-gray-200 rounded-lg aspect-square flex items-center justify-center\">\n                <span className=\"text-gray-500\">Product Image</span>\n              </div>\n\n              <div>\n                <h1 className=\"text-2xl font-bold mb-2\">Executive Anvil</h1>\n                <p className=\"text-gray-600 mb-2\">by ACME</p>\n\n                <div className=\"flex items-center gap-2 mb-4\">\n                  <div className=\"flex text-yellow-400\">{\"★★★★☆\"}</div>\n                  <span className=\"text-sm text-gray-600\">\n                    4.4 (89 reviews)\n                  </span>\n                </div>\n\n                <div className=\"space-y-2 text-sm\">\n                  <p>\n                    <strong>SKU:</strong> 0446310786\n                  </p>\n                  <p>\n                    <strong>MPN:</strong> 925872\n                  </p>\n                </div>\n              </div>\n\n              <div className=\"border-t pt-4\">\n                <h3 className=\"font-semibold mb-3\">Description</h3>\n                <p className=\"text-sm text-gray-700\">\n                  Sleeker than ACME's Classic Anvil, the Executive Anvil is\n                  perfect for the business traveler looking for something to\n                  drop from a height.\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"lg:col-span-2\">\n            <div className=\"bg-blue-50 rounded-lg p-6 mb-6\">\n              <h2 className=\"text-xl font-bold mb-4\">Price Comparison</h2>\n              <div className=\"flex items-baseline gap-4\">\n                <div>\n                  <span className=\"text-3xl font-bold\">$119.99</span>\n                  <span className=\"text-sm text-gray-600\"> - </span>\n                  <span className=\"text-3xl font-bold\">$199.99</span>\n                </div>\n                <span className=\"bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm font-medium\">\n                  5 offers available\n                </span>\n              </div>\n            </div>\n\n            <div className=\"space-y-4\">\n              <h3 className=\"text-lg font-semibold\">\n                Available from 5 sellers\n              </h3>\n\n              <div className=\"border rounded-lg p-4 hover:shadow-lg transition-shadow\">\n                <div className=\"flex items-start justify-between\">\n                  <div className=\"flex-1\">\n                    <h4 className=\"font-semibold text-lg mb-1\">ACME Direct</h4>\n                    <div className=\"flex items-center gap-4 mb-2\">\n                      <span className=\"text-2xl font-bold text-green-600\">\n                        $119.99\n                      </span>\n                      <span className=\"bg-green-100 text-green-800 px-2 py-1 rounded text-xs font-medium\">\n                        BEST PRICE\n                      </span>\n                    </div>\n                    <div className=\"flex gap-4 text-sm text-gray-600\">\n                      <span>✓ Free Shipping</span>\n                      <span>✓ In Stock</span>\n                      <span>✓ Official Store</span>\n                    </div>\n                    <p className=\"text-sm text-gray-500 mt-2\">\n                      Usually ships within 24 hours\n                    </p>\n                  </div>\n                  <button className=\"bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700\">\n                    View Deal\n                  </button>\n                </div>\n              </div>\n\n              <div className=\"border rounded-lg p-4 hover:shadow-lg transition-shadow\">\n                <div className=\"flex items-start justify-between\">\n                  <div className=\"flex-1\">\n                    <h4 className=\"font-semibold text-lg mb-1\">MegaMart</h4>\n                    <div className=\"flex items-center gap-4 mb-2\">\n                      <span className=\"text-2xl font-bold\">$129.99</span>\n                    </div>\n                    <div className=\"flex gap-4 text-sm text-gray-600\">\n                      <span>✓ Free Shipping over $100</span>\n                      <span>✓ In Stock</span>\n                    </div>\n                    <p className=\"text-sm text-gray-500 mt-2\">\n                      Ships in 2-3 business days\n                    </p>\n                  </div>\n                  <button className=\"bg-gray-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-gray-700\">\n                    View Deal\n                  </button>\n                </div>\n              </div>\n\n              <div className=\"border rounded-lg p-4 hover:shadow-lg transition-shadow\">\n                <div className=\"flex items-start justify-between\">\n                  <div className=\"flex-1\">\n                    <h4 className=\"font-semibold text-lg mb-1\">QuickShop</h4>\n                    <div className=\"flex items-center gap-4 mb-2\">\n                      <span className=\"text-2xl font-bold\">$149.99</span>\n                      <span className=\"bg-orange-100 text-orange-800 px-2 py-1 rounded text-xs font-medium\">\n                        FAST DELIVERY\n                      </span>\n                    </div>\n                    <div className=\"flex gap-4 text-sm text-gray-600\">\n                      <span>+ $9.99 Shipping</span>\n                      <span>✓ In Stock</span>\n                      <span>✓ Next Day Delivery</span>\n                    </div>\n                    <p className=\"text-sm text-gray-500 mt-2\">\n                      Order by 2pm for next day delivery\n                    </p>\n                  </div>\n                  <button className=\"bg-gray-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-gray-700\">\n                    View Deal\n                  </button>\n                </div>\n              </div>\n\n              <div className=\"border rounded-lg p-4 hover:shadow-lg transition-shadow\">\n                <div className=\"flex items-start justify-between\">\n                  <div className=\"flex-1\">\n                    <h4 className=\"font-semibold text-lg mb-1\">\n                      Hardware Haven\n                    </h4>\n                    <div className=\"flex items-center gap-4 mb-2\">\n                      <span className=\"text-2xl font-bold\">$179.99</span>\n                    </div>\n                    <div className=\"flex gap-4 text-sm text-gray-600\">\n                      <span>✓ Free Shipping</span>\n                      <span>✓ Limited Stock (3 left)</span>\n                    </div>\n                    <p className=\"text-sm text-gray-500 mt-2\">\n                      Ships in 3-5 business days\n                    </p>\n                  </div>\n                  <button className=\"bg-gray-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-gray-700\">\n                    View Deal\n                  </button>\n                </div>\n              </div>\n\n              <div className=\"border rounded-lg p-4 hover:shadow-lg transition-shadow\">\n                <div className=\"flex items-start justify-between\">\n                  <div className=\"flex-1\">\n                    <h4 className=\"font-semibold text-lg mb-1\">\n                      Premium Tools\n                    </h4>\n                    <div className=\"flex items-center gap-4 mb-2\">\n                      <span className=\"text-2xl font-bold\">$199.99</span>\n                      <span className=\"bg-purple-100 text-purple-800 px-2 py-1 rounded text-xs font-medium\">\n                        EXTENDED WARRANTY\n                      </span>\n                    </div>\n                    <div className=\"flex gap-4 text-sm text-gray-600\">\n                      <span>✓ Free Shipping</span>\n                      <span>✓ In Stock</span>\n                      <span>✓ 3-Year Warranty</span>\n                    </div>\n                    <p className=\"text-sm text-gray-500 mt-2\">\n                      Includes premium packaging and warranty\n                    </p>\n                  </div>\n                  <button className=\"bg-gray-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-gray-700\">\n                    View Deal\n                  </button>\n                </div>\n              </div>\n            </div>\n\n            <div className=\"mt-8 bg-gray-50 rounded-lg p-6\">\n              <h3 className=\"font-semibold mb-4\">Price History</h3>\n              <div className=\"h-48 bg-white rounded border flex items-center justify-center\">\n                <span className=\"text-gray-400\">\n                  Price trend chart would go here\n                </span>\n              </div>\n              <div className=\"grid grid-cols-3 gap-4 mt-4 text-sm\">\n                <div>\n                  <p className=\"text-gray-600\">Lowest Price</p>\n                  <p className=\"font-semibold\">$119.99</p>\n                </div>\n                <div>\n                  <p className=\"text-gray-600\">Average Price</p>\n                  <p className=\"font-semibold\">$159.99</p>\n                </div>\n                <div>\n                  <p className=\"text-gray-600\">Highest Price</p>\n                  <p className=\"font-semibold\">$199.99</p>\n                </div>\n              </div>\n            </div>\n\n            <div className=\"mt-8\">\n              <h3 className=\"text-lg font-semibold mb-4\">Recent Reviews</h3>\n              <div className=\"space-y-4\">\n                <div className=\"border rounded-lg p-4\">\n                  <div className=\"flex items-start justify-between mb-2\">\n                    <div>\n                      <span className=\"font-semibold\">Fred Benson</span>\n                      <span className=\"text-sm text-gray-600 ml-2\">\n                        Jan 10, 2024\n                      </span>\n                    </div>\n                    <span className=\"flex text-yellow-400\">★★★★★</span>\n                  </div>\n                  <p className=\"text-gray-700\">\n                    This anvil is perfect! Exactly what I needed for my\n                    roadrunner traps.\n                  </p>\n                </div>\n\n                <div className=\"border rounded-lg p-4\">\n                  <div className=\"flex items-start justify-between mb-2\">\n                    <div>\n                      <span className=\"font-semibold\">Wile E. Coyote</span>\n                      <span className=\"text-sm text-gray-600 ml-2\">\n                        Jan 5, 2024\n                      </span>\n                    </div>\n                    <span className=\"flex text-yellow-400\">★★★★☆</span>\n                  </div>\n                  <p className=\"text-gray-700\">\n                    Great anvil, but shipping took longer than expected.\n                  </p>\n                </div>\n              </div>\n              <button className=\"mt-4 text-blue-600 hover:underline\">\n                View all 89 reviews →\n              </button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product-certification/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function ProductCertificationPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProductJsonLd\n        name=\"Energy Efficient Refrigerator - 500L\"\n        description=\"A++ rated energy efficient refrigerator with advanced cooling technology and smart features\"\n        url=\"https://example.com/products/energy-efficient-fridge\"\n        image={[\n          \"https://example.com/fridge-1x1.jpg\",\n          \"https://example.com/fridge-4x3.jpg\",\n          \"https://example.com/fridge-16x9.jpg\",\n        ]}\n        sku=\"FRIDGE-EE-500L\"\n        mpn=\"REF500EE\"\n        gtin14=\"00012345678905\"\n        brand=\"EcoAppliances\"\n        hasCertification={[\n          {\n            issuedBy: {\n              name: \"European_Commission\",\n            },\n            name: \"EPREL\",\n            url: \"https://eprel.ec.europa.eu/screen/product/refrigerators/123456\",\n            certificationIdentification: \"123456\",\n            certificationRating: {\n              ratingValue: \"A++\",\n              bestRating: \"A+++\",\n              worstRating: \"G\",\n            },\n          },\n          {\n            issuedBy: {\n              name: \"ENERGY STAR\",\n            },\n            name: \"ENERGY_STAR_Certified\",\n            url: \"https://www.energystar.gov/products/123456\",\n          },\n        ]}\n        offers={{\n          price: 899.0,\n          priceCurrency: \"EUR\",\n          availability: \"InStock\",\n          priceValidUntil: \"2024-12-31\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.5,\n          reviewCount: 234,\n        }}\n      />\n\n      <div className=\"max-w-4xl\">\n        <nav className=\"mb-8\">\n          <ol className=\"flex space-x-2 text-sm text-gray-600\">\n            <li>Home</li>\n            <li>/</li>\n            <li>Appliances</li>\n            <li>/</li>\n            <li>Refrigerators</li>\n            <li>/</li>\n            <li className=\"font-semibold text-gray-900\">\n              Energy Efficient Refrigerator - 500L\n            </li>\n          </ol>\n        </nav>\n\n        <div className=\"grid md:grid-cols-2 gap-8\">\n          <div className=\"space-y-4\">\n            <div className=\"bg-gray-200 rounded-lg aspect-[3/4] flex items-center justify-center relative\">\n              <span className=\"text-gray-500\">Refrigerator Image</span>\n              <div className=\"absolute top-4 left-4 bg-gradient-to-br from-green-500 to-green-600 text-white px-3 py-2 rounded-lg shadow-lg\">\n                <div className=\"text-2xl font-bold\">A++</div>\n                <div className=\"text-xs\">Energy</div>\n              </div>\n            </div>\n            <div className=\"grid grid-cols-4 gap-2\">\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n            </div>\n          </div>\n\n          <div className=\"space-y-6\">\n            <div>\n              <h1 className=\"text-3xl font-bold mb-2\">\n                Energy Efficient Refrigerator - 500L\n              </h1>\n              <div className=\"flex items-center gap-4 mb-4\">\n                <div className=\"flex text-yellow-400\">{\"★★★★☆\"}</div>\n                <span className=\"text-gray-600\">\n                  4.5 out of 5 (234 reviews)\n                </span>\n              </div>\n              <p className=\"text-gray-600\">by EcoAppliances</p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <div className=\"flex items-baseline gap-2\">\n                <span className=\"text-3xl font-bold\">€899.00</span>\n                <span className=\"text-sm text-gray-500\">EUR</span>\n              </div>\n              <p className=\"text-green-600 font-medium\">✓ In Stock</p>\n              <p className=\"text-sm text-gray-500\">\n                Price valid until Dec 31, 2024\n              </p>\n            </div>\n\n            <div className=\"bg-gradient-to-r from-green-50 to-blue-50 border border-green-200 rounded-lg p-4 space-y-3\">\n              <h3 className=\"font-semibold text-green-800\">\n                Energy Certifications\n              </h3>\n              <div className=\"space-y-2\">\n                <div className=\"flex items-center justify-between bg-white rounded p-2\">\n                  <div className=\"flex items-center gap-3\">\n                    <div className=\"bg-green-500 text-white text-sm font-bold px-2 py-1 rounded\">\n                      A++\n                    </div>\n                    <div>\n                      <div className=\"font-medium\">EU Energy Label</div>\n                      <div className=\"text-xs text-gray-600\">EPREL: 123456</div>\n                    </div>\n                  </div>\n                  <a href=\"#\" className=\"text-blue-600 text-sm hover:underline\">\n                    View Certificate →\n                  </a>\n                </div>\n                <div className=\"flex items-center justify-between bg-white rounded p-2\">\n                  <div className=\"flex items-center gap-3\">\n                    <div className=\"bg-blue-500 text-white text-xs font-bold px-2 py-1 rounded\">\n                      CERTIFIED\n                    </div>\n                    <div>\n                      <div className=\"font-medium\">ENERGY STAR®</div>\n                      <div className=\"text-xs text-gray-600\">\n                        Most Efficient 2024\n                      </div>\n                    </div>\n                  </div>\n                  <a href=\"#\" className=\"text-blue-600 text-sm hover:underline\">\n                    Verify →\n                  </a>\n                </div>\n              </div>\n            </div>\n\n            <div className=\"border-l-4 border-green-500 pl-4 bg-green-50 p-3 rounded\">\n              <p className=\"font-semibold text-green-800\">\n                Save on Energy Bills\n              </p>\n              <p className=\"text-sm text-green-700\">\n                This A++ rated appliance uses 40% less energy than standard\n                models, saving approximately €120 per year\n              </p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <p className=\"text-sm\">\n                <strong>SKU:</strong> FRIDGE-EE-500L\n              </p>\n              <p className=\"text-sm\">\n                <strong>MPN:</strong> REF500EE\n              </p>\n              <p className=\"text-sm\">\n                <strong>GTIN:</strong> 00012345678905\n              </p>\n            </div>\n\n            <div className=\"space-y-3\">\n              <button className=\"w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700\">\n                Add to Cart\n              </button>\n              <button className=\"w-full border border-gray-300 py-3 px-6 rounded-lg font-medium hover:bg-gray-50\">\n                Compare Models\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Energy Performance</h2>\n          <div className=\"grid md:grid-cols-3 gap-8\">\n            <div>\n              <h3 className=\"font-semibold mb-3\">Annual Consumption</h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Energy:</dt>\n                  <dd className=\"font-medium\">180 kWh/year</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">CO2 Emissions:</dt>\n                  <dd className=\"font-medium\">90 kg/year</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Water:</dt>\n                  <dd className=\"font-medium\">N/A</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Noise Level:</dt>\n                  <dd className=\"font-medium\">38 dB</dd>\n                </div>\n              </dl>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Capacity</h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Total:</dt>\n                  <dd>500 L</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Refrigerator:</dt>\n                  <dd>350 L</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Freezer:</dt>\n                  <dd>150 L</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Climate Class:</dt>\n                  <dd>SN-T</dd>\n                </div>\n              </dl>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Features</h3>\n              <ul className=\"space-y-2 text-sm text-gray-700\">\n                <li>• No Frost technology</li>\n                <li>• Multi-zone cooling</li>\n                <li>• LED interior lighting</li>\n                <li>• Smart temperature control</li>\n                <li>• Eco mode function</li>\n              </ul>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-8 bg-blue-50 rounded-lg p-6\">\n          <h3 className=\"font-semibold mb-3\">About Energy Labels</h3>\n          <p className=\"text-sm text-gray-700 mb-3\">\n            The EU energy label provides clear and comparable information about\n            the energy consumption of appliances. The scale ranges from A+++\n            (most efficient) to G (least efficient).\n          </p>\n          <p className=\"text-sm text-gray-700\">\n            The EPREL (European Product Registry for Energy Labelling) number\n            allows you to access detailed product information in the official EU\n            database.\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product-member-pricing/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function ProductMemberPricingPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProductJsonLd\n        name=\"Premium Coffee Beans - 1kg\"\n        description=\"Single-origin Arabica coffee beans, medium roast, ethically sourced from Colombia\"\n        url=\"https://example.com/products/premium-coffee-beans\"\n        image={[\n          \"https://example.com/coffee-1x1.jpg\",\n          \"https://example.com/coffee-4x3.jpg\",\n          \"https://example.com/coffee-16x9.jpg\",\n        ]}\n        sku=\"COFFEE-COL-1KG\"\n        mpn=\"PCB1000\"\n        brand=\"Artisan Roasters\"\n        offers={{\n          url: \"https://example.com/buy/coffee-beans\",\n          price: 24.99,\n          priceCurrency: \"USD\",\n          availability: \"InStock\",\n          priceValidUntil: \"2024-12-31\",\n          // Multiple price specifications for member pricing\n          priceSpecification: [\n            {\n              price: 24.99,\n              priceCurrency: \"USD\",\n            },\n            {\n              price: 22.49,\n              priceCurrency: \"USD\",\n              validForMemberTier: {\n                \"@id\": \"https://example.com/membership#silver\",\n                name: \"Silver\",\n                hasTierBenefit: \"TierBenefitLoyaltyPrice\",\n              },\n            },\n            {\n              price: 19.99,\n              priceCurrency: \"USD\",\n              validForMemberTier: [\n                {\n                  \"@id\": \"https://example.com/membership#gold\",\n                  name: \"Gold\",\n                  hasTierBenefit: \"TierBenefitLoyaltyPrice\",\n                },\n                {\n                  \"@id\": \"https://example.com/membership#platinum\",\n                  name: \"Platinum\",\n                  hasTierBenefit: \"TierBenefitLoyaltyPrice\",\n                },\n              ],\n            },\n            {\n              membershipPointsEarned: 50,\n              validForMemberTier: {\n                \"@id\": \"https://example.com/membership#any\",\n                name: \"All Members\",\n                hasTierBenefit: \"TierBenefitLoyaltyPoints\",\n              },\n            },\n          ],\n        }}\n        aggregateRating={{\n          ratingValue: 4.8,\n          reviewCount: 156,\n        }}\n      />\n\n      <div className=\"max-w-4xl\">\n        <nav className=\"mb-8\">\n          <ol className=\"flex space-x-2 text-sm text-gray-600\">\n            <li>Home</li>\n            <li>/</li>\n            <li>Coffee</li>\n            <li>/</li>\n            <li>Whole Beans</li>\n            <li>/</li>\n            <li className=\"font-semibold text-gray-900\">\n              Premium Coffee Beans - 1kg\n            </li>\n          </ol>\n        </nav>\n\n        <div className=\"grid md:grid-cols-2 gap-8\">\n          <div className=\"space-y-4\">\n            <div className=\"bg-amber-100 rounded-lg aspect-square flex items-center justify-center\">\n              <span className=\"text-amber-700\">Coffee Bag Image</span>\n            </div>\n            <div className=\"grid grid-cols-3 gap-2\">\n              <div className=\"bg-amber-50 rounded aspect-square\"></div>\n              <div className=\"bg-amber-50 rounded aspect-square\"></div>\n              <div className=\"bg-amber-50 rounded aspect-square\"></div>\n            </div>\n          </div>\n\n          <div className=\"space-y-6\">\n            <div>\n              <h1 className=\"text-3xl font-bold mb-2\">\n                Premium Coffee Beans - 1kg\n              </h1>\n              <div className=\"flex items-center gap-4 mb-4\">\n                <div className=\"flex text-yellow-400\">{\"★★★★★\"}</div>\n                <span className=\"text-gray-600\">\n                  4.8 out of 5 (156 reviews)\n                </span>\n              </div>\n              <p className=\"text-gray-600\">by Artisan Roasters</p>\n            </div>\n\n            <div className=\"space-y-4\">\n              <div className=\"border rounded-lg p-4 bg-gradient-to-r from-yellow-50 to-amber-50\">\n                <h3 className=\"font-semibold mb-3 text-amber-900\">\n                  Member Pricing Tiers\n                </h3>\n                <div className=\"space-y-2\">\n                  <div className=\"flex justify-between items-center py-2 border-b border-amber-200\">\n                    <span className=\"text-gray-700\">Regular Price</span>\n                    <span className=\"font-bold text-lg\">$24.99</span>\n                  </div>\n                  <div className=\"flex justify-between items-center py-2 border-b border-amber-200\">\n                    <span className=\"flex items-center gap-2\">\n                      <span className=\"bg-gray-400 text-white text-xs px-2 py-1 rounded\">\n                        SILVER\n                      </span>\n                      <span className=\"text-gray-700\">Member Price</span>\n                    </span>\n                    <span className=\"font-bold text-lg text-green-600\">\n                      $22.49\n                    </span>\n                  </div>\n                  <div className=\"flex justify-between items-center py-2\">\n                    <span className=\"flex items-center gap-2\">\n                      <span className=\"bg-yellow-500 text-white text-xs px-2 py-1 rounded\">\n                        GOLD+\n                      </span>\n                      <span className=\"text-gray-700\">Premium Member</span>\n                    </span>\n                    <span className=\"font-bold text-lg text-green-600\">\n                      $19.99\n                    </span>\n                  </div>\n                </div>\n                <div className=\"mt-3 pt-3 border-t border-amber-200\">\n                  <div className=\"flex justify-between items-center\">\n                    <span className=\"text-sm text-gray-600\">\n                      Earn Loyalty Points\n                    </span>\n                    <span className=\"font-semibold text-amber-700\">\n                      +50 points\n                    </span>\n                  </div>\n                </div>\n              </div>\n\n              <p className=\"text-green-600 font-medium\">✓ In Stock</p>\n              <p className=\"text-sm text-gray-500\">\n                Member prices valid until Dec 31, 2024\n              </p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <p className=\"text-sm\">\n                <strong>SKU:</strong> COFFEE-COL-1KG\n              </p>\n              <p className=\"text-sm\">\n                <strong>MPN:</strong> PCB1000\n              </p>\n              <p className=\"text-sm\">\n                <strong>Roast Date:</strong> November 15, 2024\n              </p>\n            </div>\n\n            <div className=\"space-y-4\">\n              <p className=\"text-gray-700\">\n                Experience the rich, smooth flavor of our single-origin\n                Colombian Arabica beans. Carefully selected from high-altitude\n                farms and roasted to perfection, these beans offer notes of\n                chocolate, caramel, and subtle citrus.\n              </p>\n            </div>\n\n            <div className=\"space-y-3\">\n              <button className=\"w-full bg-amber-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-amber-700\">\n                Add to Cart\n              </button>\n              <button className=\"w-full border border-amber-600 text-amber-600 py-3 px-6 rounded-lg font-medium hover:bg-amber-50\">\n                Join Membership for Better Prices\n              </button>\n              <p className=\"text-xs text-center text-gray-500\">\n                Not a member? Join today and save up to 20% on every purchase\n              </p>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Product Details</h2>\n          <div className=\"grid md:grid-cols-2 gap-8\">\n            <div>\n              <h3 className=\"font-semibold mb-3\">Coffee Profile</h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Origin:</dt>\n                  <dd>Colombia, Huila Region</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Altitude:</dt>\n                  <dd>1,700-2,000m</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Process:</dt>\n                  <dd>Washed</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Roast Level:</dt>\n                  <dd>Medium</dd>\n                </div>\n              </dl>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Tasting Notes</h3>\n              <ul className=\"space-y-2 text-gray-700\">\n                <li>• Rich chocolate undertones</li>\n                <li>• Smooth caramel sweetness</li>\n                <li>• Bright citrus acidity</li>\n                <li>• Clean, balanced finish</li>\n              </ul>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product-review/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function ProductReviewPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProductJsonLd\n        name=\"Cheese Grater Pro\"\n        description=\"Professional-grade cheese grater for culinary enthusiasts\"\n        url=\"https://example.com/products/cheese-grater-pro\"\n        image=\"https://example.com/photos/cheese-grater.jpg\"\n        sku=\"CG-PRO-2024\"\n        brand=\"Kitchen Masters\"\n        review={{\n          name: \"Cheese Grater Pro Review\",\n          author: \"Pascal Van Cleeff\",\n          reviewRating: {\n            ratingValue: 4,\n            bestRating: 5,\n          },\n          reviewBody:\n            \"After extensive testing in my professional kitchen, I can say this is a solid cheese grater with some great features and a few drawbacks.\",\n          datePublished: \"2024-01-15\",\n          positiveNotes: {\n            itemListElement: [\n              { name: \"Consistent results\" },\n              { name: \"Still sharp after many uses\" },\n              { name: \"Easy to clean\" },\n              { name: \"Comfortable grip\" },\n              { name: \"Multiple grating sizes\" },\n            ],\n          },\n          negativeNotes: {\n            itemListElement: [\n              { name: \"No child protection\" },\n              { name: \"Lacking advanced features\" },\n              { name: \"Takes up drawer space\" },\n            ],\n          },\n        }}\n        aggregateRating={{\n          ratingValue: 4.2,\n          reviewCount: 234,\n        }}\n        offers={{\n          price: 29.99,\n          priceCurrency: \"USD\",\n          availability: \"InStock\",\n        }}\n      />\n\n      <div className=\"max-w-4xl\">\n        <nav className=\"mb-8\">\n          <ol className=\"flex space-x-2 text-sm text-gray-600\">\n            <li>Home</li>\n            <li>/</li>\n            <li>Kitchen</li>\n            <li>/</li>\n            <li>Reviews</li>\n            <li>/</li>\n            <li className=\"font-semibold text-gray-900\">\n              Cheese Grater Pro Review\n            </li>\n          </ol>\n        </nav>\n\n        <article className=\"prose lg:prose-xl max-w-none\">\n          <h1 className=\"text-4xl font-bold mb-4\">Cheese Grater Pro Review</h1>\n\n          <div className=\"bg-gray-50 rounded-lg p-6 mb-8\">\n            <div className=\"flex items-start gap-6\">\n              <div className=\"bg-gray-200 rounded-lg w-32 h-32 flex-shrink-0\"></div>\n              <div className=\"flex-1\">\n                <h2 className=\"text-2xl font-semibold mb-2\">\n                  Cheese Grater Pro\n                </h2>\n                <p className=\"text-gray-600 mb-3\">by Kitchen Masters</p>\n                <div className=\"flex items-center gap-4\">\n                  <div className=\"flex text-yellow-400\">{\"★★★★☆\"}</div>\n                  <span className=\"font-semibold\">4.0 out of 5</span>\n                </div>\n                <p className=\"text-green-600 font-medium mt-2\">\n                  $29.99 - In Stock\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"mb-8\">\n            <div className=\"flex items-center gap-4 mb-4\">\n              <div>\n                <p className=\"font-semibold\">Pascal Van Cleeff</p>\n                <p className=\"text-sm text-gray-600\">\n                  Professional Chef • Published Jan 15, 2024\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <p className=\"text-lg text-gray-700 mb-8\">\n            After extensive testing in my professional kitchen, I can say this\n            is a solid cheese grater with some great features and a few\n            drawbacks. I've used it daily for three months, grating everything\n            from hard parmesan to soft mozzarella, and it has held up remarkably\n            well.\n          </p>\n\n          <div className=\"grid md:grid-cols-2 gap-8 mb-8\">\n            <div className=\"bg-green-50 rounded-lg p-6\">\n              <h3 className=\"text-xl font-bold text-green-800 mb-4 flex items-center\">\n                <span className=\"text-2xl mr-2\">✓</span> Pros\n              </h3>\n              <ul className=\"space-y-3\">\n                <li className=\"flex items-start\">\n                  <span className=\"text-green-600 mr-2\">•</span>\n                  <div>\n                    <strong>Consistent results</strong>\n                    <p className=\"text-sm text-gray-600 mt-1\">\n                      Every grate produces uniform shreds, perfect for\n                      professional presentation\n                    </p>\n                  </div>\n                </li>\n                <li className=\"flex items-start\">\n                  <span className=\"text-green-600 mr-2\">•</span>\n                  <div>\n                    <strong>Still sharp after many uses</strong>\n                    <p className=\"text-sm text-gray-600 mt-1\">\n                      Three months of daily use and still cutting like new\n                    </p>\n                  </div>\n                </li>\n                <li className=\"flex items-start\">\n                  <span className=\"text-green-600 mr-2\">•</span>\n                  <div>\n                    <strong>Easy to clean</strong>\n                    <p className=\"text-sm text-gray-600 mt-1\">\n                      Dishwasher safe and cheese doesn't stick\n                    </p>\n                  </div>\n                </li>\n                <li className=\"flex items-start\">\n                  <span className=\"text-green-600 mr-2\">•</span>\n                  <div>\n                    <strong>Comfortable grip</strong>\n                    <p className=\"text-sm text-gray-600 mt-1\">\n                      Ergonomic handle reduces hand fatigue\n                    </p>\n                  </div>\n                </li>\n                <li className=\"flex items-start\">\n                  <span className=\"text-green-600 mr-2\">•</span>\n                  <div>\n                    <strong>Multiple grating sizes</strong>\n                    <p className=\"text-sm text-gray-600 mt-1\">\n                      Four different surfaces for various needs\n                    </p>\n                  </div>\n                </li>\n              </ul>\n            </div>\n\n            <div className=\"bg-red-50 rounded-lg p-6\">\n              <h3 className=\"text-xl font-bold text-red-800 mb-4 flex items-center\">\n                <span className=\"text-2xl mr-2\">✗</span> Cons\n              </h3>\n              <ul className=\"space-y-3\">\n                <li className=\"flex items-start\">\n                  <span className=\"text-red-600 mr-2\">•</span>\n                  <div>\n                    <strong>No child protection</strong>\n                    <p className=\"text-sm text-gray-600 mt-1\">\n                      Very sharp edges with no safety cover included\n                    </p>\n                  </div>\n                </li>\n                <li className=\"flex items-start\">\n                  <span className=\"text-red-600 mr-2\">•</span>\n                  <div>\n                    <strong>Lacking advanced features</strong>\n                    <p className=\"text-sm text-gray-600 mt-1\">\n                      No container attachment or measurement markings\n                    </p>\n                  </div>\n                </li>\n                <li className=\"flex items-start\">\n                  <span className=\"text-red-600 mr-2\">•</span>\n                  <div>\n                    <strong>Takes up drawer space</strong>\n                    <p className=\"text-sm text-gray-600 mt-1\">\n                      Bulky design doesn't fold or compress for storage\n                    </p>\n                  </div>\n                </li>\n              </ul>\n            </div>\n          </div>\n\n          <div className=\"bg-blue-50 rounded-lg p-6 mb-8\">\n            <h3 className=\"text-xl font-bold mb-4\">Detailed Analysis</h3>\n\n            <h4 className=\"font-semibold mb-2\">Build Quality</h4>\n            <p className=\"mb-4\">\n              The Cheese Grater Pro is constructed from high-grade stainless\n              steel that shows no signs of rust or wear after months of use. The\n              handle is made from a comfortable rubber compound that provides\n              excellent grip even when wet.\n            </p>\n\n            <h4 className=\"font-semibold mb-2\">Performance</h4>\n            <p className=\"mb-4\">\n              In terms of performance, this grater excels. Hard cheeses like\n              parmesan are effortlessly reduced to fine powder, while softer\n              cheeses maintain their integrity without becoming mushy. The\n              multiple grating surfaces allow for versatility in the kitchen.\n            </p>\n\n            <h4 className=\"font-semibold mb-2\">Value for Money</h4>\n            <p className=\"mb-4\">\n              At $29.99, the Cheese Grater Pro sits in the mid-range price\n              bracket. Given its durability and performance, it represents good\n              value for both home cooks and professionals.\n            </p>\n          </div>\n\n          <div className=\"bg-gray-100 rounded-lg p-6\">\n            <h3 className=\"text-xl font-bold mb-4\">Final Verdict</h3>\n            <div className=\"flex items-center gap-4 mb-4\">\n              <div className=\"flex text-yellow-400 text-2xl\">{\"★★★★☆\"}</div>\n              <span className=\"text-2xl font-bold\">4.0 out of 5</span>\n            </div>\n            <p className=\"text-lg\">\n              <strong>Recommended for:</strong> Home cooks and professionals who\n              need a reliable, durable cheese grater for daily use. The\n              consistent results and longevity make it a worthwhile investment\n              despite some minor drawbacks.\n            </p>\n            <p className=\"mt-4 text-gray-600\">\n              <strong>Not recommended for:</strong> Families with young children\n              (due to safety concerns) or those with limited storage space.\n            </p>\n          </div>\n        </article>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Other Reviews</h2>\n          <div className=\"space-y-4\">\n            <div className=\"border rounded-lg p-4\">\n              <div className=\"flex items-center justify-between mb-2\">\n                <span className=\"font-semibold\">Sarah M.</span>\n                <span className=\"flex text-yellow-400\">★★★★★</span>\n              </div>\n              <p className=\"text-gray-700\">\n                Best grater I've ever owned! Worth every penny.\n              </p>\n            </div>\n            <div className=\"border rounded-lg p-4\">\n              <div className=\"flex items-center justify-between mb-2\">\n                <span className=\"font-semibold\">Mike D.</span>\n                <span className=\"flex text-yellow-400\">★★★★☆</span>\n              </div>\n              <p className=\"text-gray-700\">\n                Great product but wish it had a protective cover.\n              </p>\n            </div>\n            <div className=\"border rounded-lg p-4\">\n              <div className=\"flex items-center justify-between mb-2\">\n                <span className=\"font-semibold\">Lisa K.</span>\n                <span className=\"flex text-yellow-400\">★★★★☆</span>\n              </div>\n              <p className=\"text-gray-700\">\n                Excellent quality, just a bit large for my kitchen drawer.\n              </p>\n            </div>\n          </div>\n          <p className=\"text-center mt-4 text-gray-600\">\n            Overall rating: 4.2 out of 5 (234 reviews)\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product-sale-pricing/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function ProductSalePricingPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProductJsonLd\n        name=\"4K Smart TV - 55 inch\"\n        description=\"Ultra HD 4K Smart TV with HDR, built-in streaming apps, and voice control\"\n        url=\"https://example.com/products/smart-tv-55\"\n        image={[\n          \"https://example.com/tv-1x1.jpg\",\n          \"https://example.com/tv-4x3.jpg\",\n          \"https://example.com/tv-16x9.jpg\",\n        ]}\n        sku=\"TV-4K-55-2024\"\n        mpn=\"STV55UHD\"\n        brand=\"TechVision\"\n        offers={{\n          url: \"https://example.com/buy/smart-tv\",\n          price: 599.99,\n          priceCurrency: \"USD\",\n          availability: \"InStock\",\n          priceValidUntil: \"2024-12-31\",\n          // Multiple price specifications for sale pricing\n          priceSpecification: [\n            {\n              price: 599.99,\n              priceCurrency: \"USD\",\n            },\n            {\n              priceType: \"https://schema.org/StrikethroughPrice\",\n              price: 899.99,\n              priceCurrency: \"USD\",\n            },\n          ],\n        }}\n        aggregateRating={{\n          ratingValue: 4.6,\n          reviewCount: 342,\n        }}\n      />\n\n      <div className=\"max-w-4xl\">\n        <nav className=\"mb-8\">\n          <ol className=\"flex space-x-2 text-sm text-gray-600\">\n            <li>Home</li>\n            <li>/</li>\n            <li>Electronics</li>\n            <li>/</li>\n            <li>TVs</li>\n            <li>/</li>\n            <li className=\"font-semibold text-gray-900\">4K Smart TV - 55\"</li>\n          </ol>\n        </nav>\n\n        <div className=\"grid md:grid-cols-2 gap-8\">\n          <div className=\"space-y-4\">\n            <div className=\"bg-gray-200 rounded-lg aspect-video flex items-center justify-center\">\n              <span className=\"text-gray-500\">TV Image</span>\n            </div>\n            <div className=\"grid grid-cols-4 gap-2\">\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n            </div>\n          </div>\n\n          <div className=\"space-y-6\">\n            <div>\n              <h1 className=\"text-3xl font-bold mb-2\">4K Smart TV - 55 inch</h1>\n              <div className=\"flex items-center gap-4 mb-4\">\n                <div className=\"flex text-yellow-400\">{\"★★★★☆\"}</div>\n                <span className=\"text-gray-600\">\n                  4.6 out of 5 (342 reviews)\n                </span>\n              </div>\n              <p className=\"text-gray-600\">by TechVision</p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <div className=\"flex items-baseline gap-3\">\n                <span className=\"text-3xl font-bold text-red-600\">$599.99</span>\n                <span className=\"text-xl text-gray-500 line-through\">\n                  $899.99\n                </span>\n              </div>\n              <div className=\"inline-block bg-red-600 text-white px-3 py-1 rounded-full text-sm font-semibold\">\n                SAVE $300 (33% OFF)\n              </div>\n              <p className=\"text-green-600 font-medium\">✓ In Stock</p>\n              <p className=\"text-sm text-gray-500\">\n                Sale price valid until Dec 31, 2024\n              </p>\n            </div>\n\n            <div className=\"border-l-4 border-red-600 pl-4 bg-red-50 p-3 rounded\">\n              <p className=\"font-semibold text-red-800\">Limited Time Offer!</p>\n              <p className=\"text-sm text-red-700\">\n                Black Friday pricing - while supplies last\n              </p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <p className=\"text-sm\">\n                <strong>SKU:</strong> TV-4K-55-2024\n              </p>\n              <p className=\"text-sm\">\n                <strong>MPN:</strong> STV55UHD\n              </p>\n            </div>\n\n            <div className=\"space-y-4\">\n              <p className=\"text-gray-700\">\n                Experience stunning picture quality with our 55-inch 4K Smart\n                TV. Features HDR10+ support, built-in streaming apps including\n                Netflix and Prime Video, and voice control compatibility with\n                Alexa and Google Assistant.\n              </p>\n            </div>\n\n            <div className=\"space-y-3\">\n              <button className=\"w-full bg-red-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-red-700\">\n                Add to Cart - Sale Price $599.99\n              </button>\n              <button className=\"w-full border border-gray-300 py-3 px-6 rounded-lg font-medium hover:bg-gray-50\">\n                Add to Wishlist\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Key Features</h2>\n          <div className=\"grid md:grid-cols-2 gap-8\">\n            <div>\n              <h3 className=\"font-semibold mb-3\">Display</h3>\n              <ul className=\"space-y-2 text-gray-700\">\n                <li>• 55\" 4K Ultra HD (3840 x 2160)</li>\n                <li>• HDR10+ and Dolby Vision</li>\n                <li>• 120Hz refresh rate</li>\n                <li>• Wide color gamut</li>\n              </ul>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Smart Features</h3>\n              <ul className=\"space-y-2 text-gray-700\">\n                <li>• Built-in WiFi and Ethernet</li>\n                <li>• Netflix, Prime Video, Disney+</li>\n                <li>• Voice control ready</li>\n                <li>• Screen mirroring support</li>\n              </ul>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product-shipping-options/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function ProductShippingOptionsPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProductJsonLd\n        name=\"Handcrafted Wooden Desk\"\n        description=\"Solid oak desk with modern design, perfect for home office setups\"\n        url=\"https://example.com/products/wooden-desk\"\n        image={[\n          \"https://example.com/desk-1x1.jpg\",\n          \"https://example.com/desk-4x3.jpg\",\n          \"https://example.com/desk-16x9.jpg\",\n        ]}\n        sku=\"DESK-OAK-001\"\n        mpn=\"WD2024OAK\"\n        brand=\"Artisan Furniture Co.\"\n        offers={{\n          price: 799.99,\n          priceCurrency: \"USD\",\n          availability: \"InStock\",\n          priceValidUntil: \"2024-12-31\",\n          // Multiple shipping options\n          shippingDetails: [\n            {\n              // Standard shipping\n              shippingRate: {\n                value: 49.99,\n                currency: \"USD\",\n              },\n              shippingDestination: {\n                addressCountry: \"US\",\n              },\n              deliveryTime: {\n                handlingTime: {\n                  minValue: 1,\n                  maxValue: 2,\n                  unitCode: \"DAY\",\n                },\n                transitTime: {\n                  minValue: 5,\n                  maxValue: 7,\n                  unitCode: \"DAY\",\n                },\n              },\n            },\n            {\n              // Express shipping\n              shippingRate: {\n                value: 99.99,\n                currency: \"USD\",\n              },\n              shippingDestination: {\n                addressCountry: \"US\",\n              },\n              deliveryTime: {\n                handlingTime: {\n                  minValue: 0,\n                  maxValue: 1,\n                  unitCode: \"DAY\",\n                },\n                transitTime: {\n                  minValue: 2,\n                  maxValue: 3,\n                  unitCode: \"DAY\",\n                },\n              },\n            },\n            {\n              // Free shipping to specific states\n              shippingRate: {\n                value: 0,\n                currency: \"USD\",\n              },\n              shippingDestination: {\n                addressCountry: \"US\",\n                addressRegion: [\"CA\", \"NY\", \"TX\"],\n              },\n              deliveryTime: {\n                handlingTime: {\n                  minValue: 2,\n                  maxValue: 3,\n                  unitCode: \"DAY\",\n                },\n                transitTime: {\n                  minValue: 7,\n                  maxValue: 10,\n                  unitCode: \"DAY\",\n                },\n              },\n            },\n            {\n              // International shipping\n              shippingRate: {\n                value: 199.99,\n                currency: \"USD\",\n              },\n              shippingDestination: [\n                {\n                  addressCountry: \"CA\",\n                },\n                {\n                  addressCountry: \"GB\",\n                },\n                {\n                  addressCountry: \"DE\",\n                },\n              ],\n              deliveryTime: {\n                handlingTime: {\n                  minValue: 2,\n                  maxValue: 3,\n                  unitCode: \"DAY\",\n                },\n                transitTime: {\n                  minValue: 10,\n                  maxValue: 21,\n                  unitCode: \"DAY\",\n                },\n              },\n            },\n          ],\n        }}\n        aggregateRating={{\n          ratingValue: 4.7,\n          reviewCount: 58,\n        }}\n      />\n\n      <div className=\"max-w-4xl\">\n        <nav className=\"mb-8\">\n          <ol className=\"flex space-x-2 text-sm text-gray-600\">\n            <li>Home</li>\n            <li>/</li>\n            <li>Furniture</li>\n            <li>/</li>\n            <li>Office</li>\n            <li>/</li>\n            <li className=\"font-semibold text-gray-900\">\n              Handcrafted Wooden Desk\n            </li>\n          </ol>\n        </nav>\n\n        <div className=\"grid md:grid-cols-2 gap-8\">\n          <div className=\"space-y-4\">\n            <div className=\"bg-amber-50 rounded-lg aspect-[4/3] flex items-center justify-center\">\n              <span className=\"text-amber-700\">Desk Image</span>\n            </div>\n            <div className=\"grid grid-cols-4 gap-2\">\n              <div className=\"bg-amber-50 rounded aspect-square\"></div>\n              <div className=\"bg-amber-50 rounded aspect-square\"></div>\n              <div className=\"bg-amber-50 rounded aspect-square\"></div>\n              <div className=\"bg-amber-50 rounded aspect-square\"></div>\n            </div>\n          </div>\n\n          <div className=\"space-y-6\">\n            <div>\n              <h1 className=\"text-3xl font-bold mb-2\">\n                Handcrafted Wooden Desk\n              </h1>\n              <div className=\"flex items-center gap-4 mb-4\">\n                <div className=\"flex text-yellow-400\">{\"★★★★★\"}</div>\n                <span className=\"text-gray-600\">4.7 out of 5 (58 reviews)</span>\n              </div>\n              <p className=\"text-gray-600\">by Artisan Furniture Co.</p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <div className=\"flex items-baseline gap-2\">\n                <span className=\"text-3xl font-bold\">$799.99</span>\n                <span className=\"text-sm text-gray-500\">USD</span>\n              </div>\n              <p className=\"text-green-600 font-medium\">✓ In Stock</p>\n              <p className=\"text-sm text-gray-500\">Ready to ship</p>\n            </div>\n\n            <div className=\"border rounded-lg p-4 space-y-3\">\n              <h3 className=\"font-semibold\">Shipping Options</h3>\n              <div className=\"space-y-3\">\n                <label className=\"flex items-start gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50\">\n                  <input\n                    type=\"radio\"\n                    name=\"shipping\"\n                    className=\"mt-1\"\n                    defaultChecked\n                  />\n                  <div className=\"flex-1\">\n                    <div className=\"flex justify-between items-start\">\n                      <div>\n                        <div className=\"font-medium\">Standard Delivery</div>\n                        <div className=\"text-sm text-gray-600\">\n                          5-7 business days\n                        </div>\n                      </div>\n                      <span className=\"font-medium\">$49.99</span>\n                    </div>\n                  </div>\n                </label>\n\n                <label className=\"flex items-start gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50\">\n                  <input type=\"radio\" name=\"shipping\" className=\"mt-1\" />\n                  <div className=\"flex-1\">\n                    <div className=\"flex justify-between items-start\">\n                      <div>\n                        <div className=\"font-medium\">Express Delivery</div>\n                        <div className=\"text-sm text-gray-600\">\n                          2-3 business days\n                        </div>\n                      </div>\n                      <span className=\"font-medium\">$99.99</span>\n                    </div>\n                  </div>\n                </label>\n\n                <label className=\"flex items-start gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50 bg-green-50\">\n                  <input type=\"radio\" name=\"shipping\" className=\"mt-1\" />\n                  <div className=\"flex-1\">\n                    <div className=\"flex justify-between items-start\">\n                      <div>\n                        <div className=\"font-medium text-green-700\">\n                          Free Shipping\n                        </div>\n                        <div className=\"text-sm text-gray-600\">\n                          7-10 business days\n                        </div>\n                        <div className=\"text-xs text-green-600 mt-1\">\n                          Available in CA, NY, TX\n                        </div>\n                      </div>\n                      <span className=\"font-medium text-green-700\">FREE</span>\n                    </div>\n                  </div>\n                </label>\n\n                <div className=\"pt-2 border-t\">\n                  <div className=\"flex items-center gap-2 text-sm text-gray-600\">\n                    <span className=\"text-blue-600\">ℹ</span>\n                    <span>\n                      International shipping available to Canada, UK, and\n                      Germany ($199.99)\n                    </span>\n                  </div>\n                </div>\n              </div>\n            </div>\n\n            <div className=\"bg-blue-50 border-l-4 border-blue-500 p-3 rounded\">\n              <p className=\"text-sm text-blue-800\">\n                <strong>White Glove Service:</strong> Professional assembly\n                available for an additional $150 in select areas\n              </p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <p className=\"text-sm\">\n                <strong>SKU:</strong> DESK-OAK-001\n              </p>\n              <p className=\"text-sm\">\n                <strong>MPN:</strong> WD2024OAK\n              </p>\n              <p className=\"text-sm\">\n                <strong>Weight:</strong> 75 lbs\n              </p>\n              <p className=\"text-sm\">\n                <strong>Dimensions:</strong> 60\"W x 30\"D x 30\"H\n              </p>\n            </div>\n\n            <div className=\"space-y-3\">\n              <button className=\"w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700\">\n                Add to Cart\n              </button>\n              <button className=\"w-full border border-gray-300 py-3 px-6 rounded-lg font-medium hover:bg-gray-50\">\n                Check Delivery to Your Area\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Shipping Information</h2>\n          <div className=\"grid md:grid-cols-2 gap-8\">\n            <div>\n              <h3 className=\"font-semibold mb-3\">Delivery Details</h3>\n              <ul className=\"space-y-2 text-sm text-gray-700\">\n                <li>• Handling time: 1-3 business days</li>\n                <li>• Ships from: Austin, TX warehouse</li>\n                <li>• Signature required on delivery</li>\n                <li>• Tracking provided via email</li>\n                <li>• Insurance included on all shipments</li>\n              </ul>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Shipping Zones</h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div>\n                  <dt className=\"font-medium text-gray-700\">Continental US:</dt>\n                  <dd className=\"text-gray-600 ml-4\">\n                    All shipping options available\n                  </dd>\n                </div>\n                <div>\n                  <dt className=\"font-medium text-gray-700\">\n                    Alaska & Hawaii:\n                  </dt>\n                  <dd className=\"text-gray-600 ml-4\">\n                    Standard shipping only (+$50)\n                  </dd>\n                </div>\n                <div>\n                  <dt className=\"font-medium text-gray-700\">International:</dt>\n                  <dd className=\"text-gray-600 ml-4\">\n                    Select countries, 10-21 days\n                  </dd>\n                </div>\n              </dl>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-8 bg-gray-50 rounded-lg p-6\">\n          <h3 className=\"font-semibold mb-3\">Product Details</h3>\n          <p className=\"text-gray-700 mb-4\">\n            This handcrafted wooden desk is made from sustainably sourced solid\n            oak, featuring a modern minimalist design that complements any home\n            office. Each piece is unique, showcasing the natural wood grain and\n            finished with eco-friendly Danish oil for durability and beauty.\n          </p>\n          <div className=\"grid grid-cols-2 gap-4 text-sm\">\n            <div>\n              <strong>Material:</strong> Solid Oak Wood\n            </div>\n            <div>\n              <strong>Finish:</strong> Natural Danish Oil\n            </div>\n            <div>\n              <strong>Assembly:</strong> Required (tools included)\n            </div>\n            <div>\n              <strong>Warranty:</strong> 5 years limited\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product-unit-pricing/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function ProductUnitPricingPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProductJsonLd\n        name=\"Premium Olive Oil - Extra Virgin\"\n        description=\"Cold-pressed extra virgin olive oil from organic Italian olives, perfect for cooking and dressing\"\n        url=\"https://example.com/products/olive-oil\"\n        image={[\n          \"https://example.com/olive-oil-1x1.jpg\",\n          \"https://example.com/olive-oil-4x3.jpg\",\n          \"https://example.com/olive-oil-16x9.jpg\",\n        ]}\n        sku=\"OIL-EVOO-750\"\n        mpn=\"EVOO750ML\"\n        brand=\"Mediterranean Gold\"\n        offers={{\n          url: \"https://example.com/buy/olive-oil\",\n          priceSpecification: {\n            price: 18.0,\n            priceCurrency: \"EUR\",\n            // Unit pricing for products sold by volume\n            referenceQuantity: {\n              value: \"750\",\n              unitCode: \"MLT\",\n              valueReference: {\n                value: \"100\",\n                unitCode: \"MLT\",\n              },\n            },\n          },\n          availability: \"InStock\",\n          priceValidUntil: \"2024-12-31\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.9,\n          reviewCount: 89,\n        }}\n      />\n\n      <div className=\"max-w-4xl\">\n        <nav className=\"mb-8\">\n          <ol className=\"flex space-x-2 text-sm text-gray-600\">\n            <li>Home</li>\n            <li>/</li>\n            <li>Groceries</li>\n            <li>/</li>\n            <li>Oils & Vinegars</li>\n            <li>/</li>\n            <li className=\"font-semibold text-gray-900\">\n              Premium Olive Oil - Extra Virgin\n            </li>\n          </ol>\n        </nav>\n\n        <div className=\"grid md:grid-cols-2 gap-8\">\n          <div className=\"space-y-4\">\n            <div className=\"bg-olive-100 rounded-lg aspect-[3/4] flex items-center justify-center\">\n              <span className=\"text-olive-700\">Olive Oil Bottle</span>\n            </div>\n            <div className=\"grid grid-cols-3 gap-2\">\n              <div className=\"bg-olive-50 rounded aspect-square\"></div>\n              <div className=\"bg-olive-50 rounded aspect-square\"></div>\n              <div className=\"bg-olive-50 rounded aspect-square\"></div>\n            </div>\n          </div>\n\n          <div className=\"space-y-6\">\n            <div>\n              <h1 className=\"text-3xl font-bold mb-2\">\n                Premium Olive Oil - Extra Virgin\n              </h1>\n              <div className=\"flex items-center gap-4 mb-4\">\n                <div className=\"flex text-yellow-400\">{\"★★★★★\"}</div>\n                <span className=\"text-gray-600\">4.9 out of 5 (89 reviews)</span>\n              </div>\n              <p className=\"text-gray-600\">by Mediterranean Gold</p>\n            </div>\n\n            <div className=\"space-y-3\">\n              <div className=\"bg-green-50 border border-green-200 rounded-lg p-4\">\n                <div className=\"flex justify-between items-baseline mb-2\">\n                  <span className=\"text-3xl font-bold\">€18.00</span>\n                  <span className=\"text-gray-600\">750ml bottle</span>\n                </div>\n                <div className=\"text-sm text-gray-600 border-t pt-2\">\n                  <span className=\"font-medium\">Unit price: </span>\n                  <span>€2.40 per 100ml</span>\n                </div>\n              </div>\n              <p className=\"text-green-600 font-medium\">✓ In Stock</p>\n              <p className=\"text-sm text-gray-500\">\n                Price valid until Dec 31, 2024\n              </p>\n            </div>\n\n            <div className=\"bg-blue-50 border-l-4 border-blue-500 p-3 rounded\">\n              <p className=\"text-sm text-blue-800\">\n                <strong>EU Regulation:</strong> Unit pricing displayed as\n                required for products sold by weight or volume\n              </p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <p className=\"text-sm\">\n                <strong>SKU:</strong> OIL-EVOO-750\n              </p>\n              <p className=\"text-sm\">\n                <strong>MPN:</strong> EVOO750ML\n              </p>\n              <p className=\"text-sm\">\n                <strong>Volume:</strong> 750ml (25.4 fl oz)\n              </p>\n              <p className=\"text-sm\">\n                <strong>Harvest Date:</strong> October 2024\n              </p>\n            </div>\n\n            <div className=\"space-y-4\">\n              <p className=\"text-gray-700\">\n                Our premium extra virgin olive oil is cold-pressed from\n                hand-picked organic olives grown in the sun-drenched groves of\n                Tuscany. With its rich, fruity flavor and low acidity (0.3%),\n                it's perfect for drizzling over salads, dipping bread, or\n                finishing your favorite dishes.\n              </p>\n            </div>\n\n            <div className=\"space-y-3\">\n              <div className=\"flex gap-3\">\n                <select className=\"border rounded px-3 py-2\">\n                  <option>1 bottle</option>\n                  <option>3 bottles (5% off)</option>\n                  <option>6 bottles (10% off)</option>\n                </select>\n                <button className=\"flex-1 bg-green-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-green-700\">\n                  Add to Cart\n                </button>\n              </div>\n              <button className=\"w-full border border-gray-300 py-3 px-6 rounded-lg font-medium hover:bg-gray-50\">\n                Subscribe & Save 15%\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Product Information</h2>\n          <div className=\"grid md:grid-cols-3 gap-8\">\n            <div>\n              <h3 className=\"font-semibold mb-3\">Quality Indicators</h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Acidity:</dt>\n                  <dd>&lt; 0.3%</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Peroxide:</dt>\n                  <dd>&lt; 10 meq O2/kg</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Polyphenols:</dt>\n                  <dd>&gt; 250 mg/kg</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Harvest Method:</dt>\n                  <dd>Hand-picked</dd>\n                </div>\n              </dl>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">\n                Nutritional Info (per 100ml)\n              </h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Energy:</dt>\n                  <dd>884 kcal</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Total Fat:</dt>\n                  <dd>100g</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Saturated:</dt>\n                  <dd>14g</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Vitamin E:</dt>\n                  <dd>14mg</dd>\n                </div>\n              </dl>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Storage & Usage</h3>\n              <ul className=\"space-y-2 text-sm text-gray-700\">\n                <li>• Store in cool, dark place</li>\n                <li>• Best before 24 months from harvest</li>\n                <li>• Ideal for salads and finishing</li>\n                <li>• Smoke point: 190°C (374°F)</li>\n              </ul>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product-variants/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function ProductVariantsPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProductJsonLd\n        type=\"ProductGroup\"\n        name=\"Wool Winter Coat\"\n        description=\"Premium wool coat designed for cold winter climates. Available in multiple colors and sizes.\"\n        url=\"https://example.com/products/wool-winter-coat\"\n        productGroupID=\"WC2024\"\n        brand=\"Nordic Style\"\n        pattern=\"striped\"\n        material=\"wool\"\n        variesBy={[\"size\", \"color\"]}\n        aggregateRating={{\n          ratingValue: 4.6,\n          reviewCount: 127,\n        }}\n        hasVariant={[\n          {\n            name: \"Wool Winter Coat - Small Green\",\n            sku: \"WC2024-S-GRN\",\n            gtin14: \"98766051104214\",\n            size: \"small\",\n            color: \"Green\",\n            image: \"https://example.com/images/coat-small-green.jpg\",\n            offers: {\n              price: 119.99,\n              priceCurrency: \"USD\",\n              availability: \"InStock\",\n              url: \"https://example.com/products/wool-winter-coat?size=small&color=green\",\n            },\n          },\n          {\n            name: \"Wool Winter Coat - Small Light Blue\",\n            sku: \"WC2024-S-BLU\",\n            gtin14: \"98766051104207\",\n            size: \"small\",\n            color: \"Light Blue\",\n            image: \"https://example.com/images/coat-small-lightblue.jpg\",\n            offers: {\n              price: 119.99,\n              priceCurrency: \"USD\",\n              availability: \"InStock\",\n              url: \"https://example.com/products/wool-winter-coat?size=small&color=lightblue\",\n            },\n          },\n          {\n            name: \"Wool Winter Coat - Medium Green\",\n            sku: \"WC2024-M-GRN\",\n            gtin14: \"98766051104221\",\n            size: \"medium\",\n            color: \"Green\",\n            image: \"https://example.com/images/coat-medium-green.jpg\",\n            offers: {\n              price: 129.99,\n              priceCurrency: \"USD\",\n              availability: \"InStock\",\n              url: \"https://example.com/products/wool-winter-coat?size=medium&color=green\",\n            },\n          },\n          {\n            name: \"Wool Winter Coat - Large Light Blue\",\n            sku: \"WC2024-L-BLU\",\n            gtin14: \"98766051104399\",\n            size: \"large\",\n            color: \"Light Blue\",\n            image: \"https://example.com/images/coat-large-lightblue.jpg\",\n            offers: {\n              price: 139.99,\n              priceCurrency: \"USD\",\n              availability: \"BackOrder\",\n              url: \"https://example.com/products/wool-winter-coat?size=large&color=lightblue\",\n            },\n          },\n        ]}\n      />\n\n      <div className=\"max-w-6xl\">\n        <nav className=\"mb-8\">\n          <ol className=\"flex space-x-2 text-sm text-gray-600\">\n            <li>Home</li>\n            <li>/</li>\n            <li>Clothing</li>\n            <li>/</li>\n            <li>Outerwear</li>\n            <li>/</li>\n            <li className=\"font-semibold text-gray-900\">Wool Winter Coat</li>\n          </ol>\n        </nav>\n\n        <div className=\"grid md:grid-cols-2 gap-8\">\n          <div className=\"space-y-4\">\n            <div className=\"bg-gray-200 rounded-lg aspect-square flex items-center justify-center\">\n              <span className=\"text-gray-500\">Product Image</span>\n            </div>\n            <div className=\"grid grid-cols-4 gap-2\">\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n            </div>\n          </div>\n\n          <div className=\"space-y-6\">\n            <div>\n              <h1 className=\"text-3xl font-bold mb-2\">Wool Winter Coat</h1>\n              <div className=\"flex items-center gap-4 mb-4\">\n                <div className=\"flex text-yellow-400\">{\"★★★★★\"}</div>\n                <span className=\"text-gray-600\">\n                  4.6 out of 5 (127 reviews)\n                </span>\n              </div>\n              <p className=\"text-gray-600\">by Nordic Style</p>\n            </div>\n\n            <div>\n              <p className=\"text-gray-700\">\n                Premium wool coat designed for cold winter climates. Features a\n                classic striped pattern and exceptional warmth. Available in\n                multiple colors and sizes to suit your style.\n              </p>\n            </div>\n\n            <div className=\"space-y-4\">\n              <div>\n                <label className=\"block text-sm font-medium mb-2\">Color</label>\n                <div className=\"flex gap-2\">\n                  <button className=\"px-4 py-2 border-2 border-blue-500 rounded\">\n                    Light Blue\n                  </button>\n                  <button className=\"px-4 py-2 border rounded hover:border-gray-400\">\n                    Green\n                  </button>\n                </div>\n              </div>\n\n              <div>\n                <label className=\"block text-sm font-medium mb-2\">Size</label>\n                <div className=\"flex gap-2\">\n                  <button className=\"px-4 py-2 border rounded hover:border-gray-400\">\n                    Small\n                  </button>\n                  <button className=\"px-4 py-2 border rounded hover:border-gray-400\">\n                    Medium\n                  </button>\n                  <button className=\"px-4 py-2 border-2 border-blue-500 rounded\">\n                    Large\n                  </button>\n                </div>\n              </div>\n            </div>\n\n            <div className=\"space-y-2\">\n              <div className=\"flex items-baseline gap-2\">\n                <span className=\"text-3xl font-bold\">$139.99</span>\n                <span className=\"text-sm text-gray-500\">USD</span>\n              </div>\n              <p className=\"text-orange-600 font-medium\">\n                ⚠ Back Order - Ships in 2-3 weeks\n              </p>\n              <p className=\"text-sm text-gray-500\">\n                Product ID: WC2024 | SKU: WC2024-L-BLU\n              </p>\n            </div>\n\n            <div className=\"space-y-3\">\n              <button className=\"w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700\">\n                Add to Cart\n              </button>\n              <button className=\"w-full border border-gray-300 py-3 px-6 rounded-lg font-medium hover:bg-gray-50\">\n                View All Variants\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Product Variants</h2>\n          <div className=\"grid md:grid-cols-2 lg:grid-cols-4 gap-4\">\n            <div className=\"border rounded-lg p-4\">\n              <div className=\"bg-gray-200 rounded aspect-square mb-3\"></div>\n              <h3 className=\"font-semibold\">Small - Green</h3>\n              <p className=\"text-gray-600\">$119.99</p>\n              <p className=\"text-green-600 text-sm\">✓ In Stock</p>\n            </div>\n            <div className=\"border rounded-lg p-4\">\n              <div className=\"bg-gray-200 rounded aspect-square mb-3\"></div>\n              <h3 className=\"font-semibold\">Small - Light Blue</h3>\n              <p className=\"text-gray-600\">$119.99</p>\n              <p className=\"text-green-600 text-sm\">✓ In Stock</p>\n            </div>\n            <div className=\"border rounded-lg p-4\">\n              <div className=\"bg-gray-200 rounded aspect-square mb-3\"></div>\n              <h3 className=\"font-semibold\">Medium - Green</h3>\n              <p className=\"text-gray-600\">$129.99</p>\n              <p className=\"text-green-600 text-sm\">✓ In Stock</p>\n            </div>\n            <div className=\"border rounded-lg p-4 border-blue-500\">\n              <div className=\"bg-gray-200 rounded aspect-square mb-3\"></div>\n              <h3 className=\"font-semibold\">Large - Light Blue</h3>\n              <p className=\"text-gray-600\">$139.99</p>\n              <p className=\"text-orange-600 text-sm\">⚠ Back Order</p>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Product Details</h2>\n          <div className=\"grid md:grid-cols-2 gap-8\">\n            <div>\n              <h3 className=\"font-semibold mb-3\">Features</h3>\n              <ul className=\"space-y-2 text-gray-700\">\n                <li>• Premium wool construction</li>\n                <li>• Striped pattern design</li>\n                <li>• Wind and water resistant</li>\n                <li>• Interior fleece lining</li>\n                <li>• Multiple pockets with zippers</li>\n              </ul>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Specifications</h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Material:</dt>\n                  <dd>100% Wool</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Pattern:</dt>\n                  <dd>Striped</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Brand:</dt>\n                  <dd>Nordic Style</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Product Group ID:</dt>\n                  <dd>WC2024</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Available Sizes:</dt>\n                  <dd>Small, Medium, Large</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Available Colors:</dt>\n                  <dd>Green, Light Blue</dd>\n                </div>\n              </dl>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product-variants-advanced/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function ProductVariantsAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProductJsonLd\n        type=\"ProductGroup\"\n        name=\"Athletic Performance Shoes\"\n        description=\"High-performance running shoes with advanced cushioning technology. Available in multiple sizes, colors, and widths for the perfect fit.\"\n        url=\"https://example.com/products/athletic-performance-shoes\"\n        productGroupID=\"APS2024\"\n        brand={{\n          \"@type\": \"Organization\",\n          name: \"SpeedRunner Pro\",\n          logo: \"https://example.com/logos/speedrunner.png\",\n        }}\n        material=\"Mesh/Synthetic\"\n        pattern=\"solid\"\n        category=\"Sports/Running/Footwear\"\n        variesBy={[\"size\", \"color\", \"suggestedGender\"]}\n        audience={{\n          \"@type\": \"PeopleAudience\",\n          suggestedGender: \"unisex\",\n          suggestedAge: {\n            \"@type\": \"QuantitativeValue\",\n            minValue: 13,\n            unitCode: \"ANN\",\n          },\n        }}\n        aggregateRating={{\n          ratingValue: 4.8,\n          ratingCount: 342,\n          reviewCount: 289,\n        }}\n        review={[\n          {\n            name: \"Best Running Shoes Ever!\",\n            reviewRating: {\n              ratingValue: 5,\n              bestRating: 5,\n            },\n            author: \"Sarah Johnson\",\n            reviewBody:\n              \"These shoes are incredibly comfortable and provide excellent support during long runs.\",\n            datePublished: \"2024-01-15\",\n          },\n          {\n            name: \"Great for Marathon Training\",\n            reviewRating: {\n              ratingValue: 4,\n              bestRating: 5,\n            },\n            author: {\n              name: \"Mike Chen\",\n              url: \"https://example.com/users/mikechen\",\n            },\n            reviewBody:\n              \"Excellent cushioning and durability. The only downside is they run slightly narrow.\",\n            datePublished: \"2024-01-20\",\n          },\n        ]}\n        hasVariant={[\n          {\n            name: \"Athletic Performance Shoes - Men's 10 Black\",\n            sku: \"APS2024-M10-BLK\",\n            gtin14: \"98766051104444\",\n            size: \"10\",\n            color: \"Black\",\n            pattern: \"solid\",\n            image: [\n              \"https://example.com/images/shoes-m10-black-1.jpg\",\n              \"https://example.com/images/shoes-m10-black-2.jpg\",\n              \"https://example.com/images/shoes-m10-black-3.jpg\",\n            ],\n            weight: {\n              value: 310,\n              unitCode: \"GRM\",\n            },\n            offers: {\n              price: 149.99,\n              priceCurrency: \"USD\",\n              availability: \"InStock\",\n              url: \"https://example.com/products/athletic-performance-shoes?size=10&color=black&gender=mens\",\n              priceValidUntil: \"2024-12-31\",\n              seller: {\n                name: \"Official SpeedRunner Store\",\n                url: \"https://example.com\",\n              },\n              shippingDetails: {\n                \"@type\": \"OfferShippingDetails\",\n                shippingRate: {\n                  \"@type\": \"MonetaryAmount\",\n                  value: 0,\n                  currency: \"USD\",\n                },\n                shippingDestination: {\n                  \"@type\": \"DefinedRegion\",\n                  addressCountry: \"US\",\n                },\n                deliveryTime: {\n                  \"@type\": \"ShippingDeliveryTime\",\n                  handlingTime: {\n                    \"@type\": \"QuantitativeValue\",\n                    minValue: 0,\n                    maxValue: 1,\n                    unitCode: \"DAY\",\n                  },\n                  transitTime: {\n                    \"@type\": \"QuantitativeValue\",\n                    minValue: 2,\n                    maxValue: 5,\n                    unitCode: \"DAY\",\n                  },\n                },\n              },\n              hasMerchantReturnPolicy: {\n                applicableCountry: \"US\",\n                returnPolicyCategory:\n                  \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n                merchantReturnDays: 60,\n                returnMethod: \"https://schema.org/ReturnByMail\",\n                returnFees: \"https://schema.org/FreeReturn\",\n              },\n            },\n          },\n          {\n            name: \"Athletic Performance Shoes - Women's 8 White/Pink\",\n            sku: \"APS2024-W8-WPK\",\n            gtin14: \"98766051104451\",\n            size: \"8\",\n            color: \"White/Pink\",\n            pattern: \"two-tone\",\n            image: \"https://example.com/images/shoes-w8-white-pink.jpg\",\n            weight: {\n              value: 280,\n              unitCode: \"GRM\",\n            },\n            offers: {\n              price: 149.99,\n              priceCurrency: \"USD\",\n              availability: \"InStock\",\n              url: \"https://example.com/products/athletic-performance-shoes?size=8&color=white-pink&gender=womens\",\n            },\n          },\n          {\n            name: \"Athletic Performance Shoes - Men's 11 Navy\",\n            sku: \"APS2024-M11-NVY\",\n            gtin14: \"98766051104468\",\n            size: \"11\",\n            color: \"Navy\",\n            pattern: \"solid\",\n            offers: {\n              price: 139.99,\n              priceCurrency: \"USD\",\n              availability: \"PreOrder\",\n              availabilityStarts: \"2024-02-15\",\n              url: \"https://example.com/products/athletic-performance-shoes?size=11&color=navy&gender=mens\",\n            },\n          },\n          // URL-only variants for other sizes/colors available on separate pages\n          {\n            url: \"https://example.com/products/athletic-performance-shoes/mens-9-gray\",\n          },\n          {\n            url: \"https://example.com/products/athletic-performance-shoes/womens-7-purple\",\n          },\n          {\n            url: \"https://example.com/products/athletic-performance-shoes/mens-12-red\",\n          },\n        ]}\n      />\n\n      <div className=\"max-w-6xl\">\n        <nav className=\"mb-8\">\n          <ol className=\"flex space-x-2 text-sm text-gray-600\">\n            <li>Home</li>\n            <li>/</li>\n            <li>Sports</li>\n            <li>/</li>\n            <li>Running</li>\n            <li>/</li>\n            <li className=\"font-semibold text-gray-900\">\n              Athletic Performance Shoes\n            </li>\n          </ol>\n        </nav>\n\n        <div className=\"grid md:grid-cols-2 gap-8\">\n          <div className=\"space-y-4\">\n            <div className=\"bg-gray-200 rounded-lg aspect-square flex items-center justify-center\">\n              <span className=\"text-gray-500\">Product Image Gallery</span>\n            </div>\n            <div className=\"grid grid-cols-5 gap-2\">\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n            </div>\n          </div>\n\n          <div className=\"space-y-6\">\n            <div>\n              <h1 className=\"text-3xl font-bold mb-2\">\n                Athletic Performance Shoes\n              </h1>\n              <div className=\"flex items-center gap-4 mb-4\">\n                <div className=\"flex text-yellow-400\">{\"★★★★★\"}</div>\n                <span className=\"text-gray-600\">\n                  4.8 out of 5 (342 ratings, 289 reviews)\n                </span>\n              </div>\n              <div className=\"flex items-center gap-4\">\n                <p className=\"text-gray-600\">by SpeedRunner Pro</p>\n                <span className=\"text-sm bg-green-100 text-green-800 px-2 py-1 rounded\">\n                  Unisex\n                </span>\n              </div>\n            </div>\n\n            <div>\n              <p className=\"text-gray-700\">\n                High-performance running shoes with advanced cushioning\n                technology. Engineered for serious runners who demand comfort,\n                support, and durability. Features our proprietary CloudFoam™\n                midsole and breathable mesh upper.\n              </p>\n            </div>\n\n            <div className=\"space-y-4\">\n              <div>\n                <label className=\"block text-sm font-medium mb-2\">Gender</label>\n                <div className=\"flex gap-2\">\n                  <button className=\"px-4 py-2 border-2 border-blue-500 rounded\">\n                    Men's\n                  </button>\n                  <button className=\"px-4 py-2 border rounded hover:border-gray-400\">\n                    Women's\n                  </button>\n                </div>\n              </div>\n\n              <div>\n                <label className=\"block text-sm font-medium mb-2\">Size</label>\n                <div className=\"grid grid-cols-5 gap-2\">\n                  {[\"7\", \"8\", \"9\", \"10\", \"11\", \"12\", \"13\"].map((size) => (\n                    <button\n                      key={size}\n                      className={`px-3 py-2 border rounded hover:border-gray-400 ${\n                        size === \"10\" ? \"border-2 border-blue-500\" : \"\"\n                      }`}\n                    >\n                      {size}\n                    </button>\n                  ))}\n                </div>\n              </div>\n\n              <div>\n                <label className=\"block text-sm font-medium mb-2\">Color</label>\n                <div className=\"flex gap-2\">\n                  <button className=\"w-8 h-8 bg-black rounded-full border-2 border-blue-500\"></button>\n                  <button className=\"w-8 h-8 bg-gray-700 rounded-full border hover:border-gray-400\"></button>\n                  <button className=\"w-8 h-8 bg-red-600 rounded-full border hover:border-gray-400\"></button>\n                  <button className=\"w-8 h-8 bg-white rounded-full border hover:border-gray-400\"></button>\n                  <button className=\"w-8 h-8 bg-purple-600 rounded-full border hover:border-gray-400\"></button>\n                </div>\n              </div>\n            </div>\n\n            <div className=\"space-y-2\">\n              <div className=\"flex items-baseline gap-2\">\n                <span className=\"text-3xl font-bold\">$149.99</span>\n                <span className=\"text-sm text-gray-500 line-through\">\n                  $179.99\n                </span>\n                <span className=\"text-sm bg-red-100 text-red-800 px-2 py-1 rounded\">\n                  SALE\n                </span>\n              </div>\n              <p className=\"text-green-600 font-medium\">\n                ✓ In Stock - Ships today!\n              </p>\n              <p className=\"text-sm text-gray-500\">\n                FREE shipping on orders over $50\n              </p>\n              <p className=\"text-sm text-gray-500\">\n                Product ID: APS2024 | SKU: APS2024-M10-BLK | Weight: 310g\n              </p>\n            </div>\n\n            <div className=\"bg-blue-50 p-4 rounded-lg\">\n              <h3 className=\"font-semibold mb-2\">60-Day Return Policy</h3>\n              <p className=\"text-sm text-gray-700\">\n                Not satisfied? Return for free within 60 days. No questions\n                asked.\n              </p>\n            </div>\n\n            <div className=\"space-y-3\">\n              <button className=\"w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700\">\n                Add to Cart\n              </button>\n              <button className=\"w-full border border-gray-300 py-3 px-6 rounded-lg font-medium hover:bg-gray-50\">\n                Try Virtual Fitting\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Customer Reviews</h2>\n          <div className=\"space-y-6\">\n            <div className=\"border rounded-lg p-6\">\n              <div className=\"flex items-start justify-between mb-3\">\n                <div>\n                  <h3 className=\"font-semibold\">Best Running Shoes Ever!</h3>\n                  <div className=\"flex items-center gap-2 mt-1\">\n                    <div className=\"flex text-yellow-400 text-sm\">\n                      {\"★★★★★\"}\n                    </div>\n                    <span className=\"text-sm text-gray-600\">5 out of 5</span>\n                  </div>\n                </div>\n                <span className=\"text-sm text-gray-500\">Jan 15, 2024</span>\n              </div>\n              <p className=\"text-gray-700 mb-2\">\n                These shoes are incredibly comfortable and provide excellent\n                support during long runs.\n              </p>\n              <p className=\"text-sm text-gray-600\">— Sarah Johnson</p>\n            </div>\n\n            <div className=\"border rounded-lg p-6\">\n              <div className=\"flex items-start justify-between mb-3\">\n                <div>\n                  <h3 className=\"font-semibold\">Great for Marathon Training</h3>\n                  <div className=\"flex items-center gap-2 mt-1\">\n                    <div className=\"flex text-yellow-400 text-sm\">\n                      {\"★★★★☆\"}\n                    </div>\n                    <span className=\"text-sm text-gray-600\">4 out of 5</span>\n                  </div>\n                </div>\n                <span className=\"text-sm text-gray-500\">Jan 20, 2024</span>\n              </div>\n              <p className=\"text-gray-700 mb-2\">\n                Excellent cushioning and durability. The only downside is they\n                run slightly narrow.\n              </p>\n              <p className=\"text-sm text-gray-600\">\n                —{\" \"}\n                <a\n                  href=\"https://example.com/users/mikechen\"\n                  className=\"text-blue-600 hover:underline\"\n                >\n                  Mike Chen\n                </a>\n              </p>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Technical Specifications</h2>\n          <div className=\"grid md:grid-cols-3 gap-8\">\n            <div>\n              <h3 className=\"font-semibold mb-3\">Construction</h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Upper Material:</dt>\n                  <dd>Breathable Mesh</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Midsole:</dt>\n                  <dd>CloudFoam™ Technology</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Outsole:</dt>\n                  <dd>Rubber with traction pattern</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Closure:</dt>\n                  <dd>Lace-up</dd>\n                </div>\n              </dl>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Performance</h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Drop:</dt>\n                  <dd>8mm</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Weight:</dt>\n                  <dd>280-310g (size dependent)</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Cushioning:</dt>\n                  <dd>Maximum</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Support:</dt>\n                  <dd>Neutral</dd>\n                </div>\n              </dl>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Availability</h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Sizes:</dt>\n                  <dd>7-13 (Men's), 5-11 (Women's)</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Colors:</dt>\n                  <dd>6 options</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Product Group:</dt>\n                  <dd>APS2024</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Ages:</dt>\n                  <dd>13+</dd>\n                </div>\n              </dl>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product-variants-multipage/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function ProductVariantsMultipagePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      {/* This demonstrates the multi-page approach where a specific variant\n          is shown on its own page with reference to the parent ProductGroup */}\n      <ProductJsonLd\n        name=\"Premium Leather Wallet - Brown Classic\"\n        description=\"Handcrafted genuine leather wallet in classic brown. Features multiple card slots, bill compartment, and RFID blocking technology.\"\n        url=\"https://example.com/products/leather-wallet/brown-classic\"\n        sku=\"LW2024-BRN-CLS\"\n        gtin14=\"98766051105555\"\n        color=\"Brown\"\n        pattern=\"Classic\"\n        size=\"Standard\"\n        material=\"Genuine Leather\"\n        brand=\"Craftsman Leather Co.\"\n        image={[\n          \"https://example.com/images/wallet-brown-classic-1.jpg\",\n          \"https://example.com/images/wallet-brown-classic-2.jpg\",\n          \"https://example.com/images/wallet-brown-classic-3.jpg\",\n        ]}\n        // Reference to the parent ProductGroup\n        isVariantOf={{\n          \"@id\": \"#wallet_group\",\n        }}\n        inProductGroupWithID=\"LW2024\"\n        offers={{\n          price: 79.99,\n          priceCurrency: \"USD\",\n          availability: \"InStock\",\n          url: \"https://example.com/products/leather-wallet/brown-classic\",\n          seller: {\n            name: \"Craftsman Leather Co.\",\n            url: \"https://example.com\",\n          },\n        }}\n        aggregateRating={{\n          ratingValue: 4.7,\n          reviewCount: 89,\n        }}\n      />\n\n      {/* Include the ProductGroup definition on the same page for context\n          In a real multi-page scenario, this would be duplicated across variant pages */}\n      <script\n        type=\"application/ld+json\"\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify({\n            \"@context\": \"https://schema.org\",\n            \"@type\": \"ProductGroup\",\n            \"@id\": \"#wallet_group\",\n            name: \"Premium Leather Wallet\",\n            description:\n              \"Handcrafted genuine leather wallets available in multiple colors and patterns.\",\n            brand: {\n              \"@type\": \"Brand\",\n              name: \"Craftsman Leather Co.\",\n            },\n            productGroupID: \"LW2024\",\n            material: \"Genuine Leather\",\n            variesBy: [\n              \"https://schema.org/color\",\n              \"https://schema.org/pattern\",\n            ],\n            hasVariant: [\n              {\n                \"@type\": \"Product\",\n                name: \"Premium Leather Wallet - Brown Classic\",\n                sku: \"LW2024-BRN-CLS\",\n                color: \"Brown\",\n                pattern: \"Classic\",\n                offers: {\n                  \"@type\": \"Offer\",\n                  price: 79.99,\n                  priceCurrency: \"USD\",\n                  url: \"https://example.com/products/leather-wallet/brown-classic\",\n                },\n              },\n              {\n                url: \"https://example.com/products/leather-wallet/black-modern\",\n              },\n              {\n                url: \"https://example.com/products/leather-wallet/tan-vintage\",\n              },\n            ],\n          }),\n        }}\n      />\n\n      <div className=\"max-w-6xl\">\n        <nav className=\"mb-8\">\n          <ol className=\"flex space-x-2 text-sm text-gray-600\">\n            <li>Home</li>\n            <li>/</li>\n            <li>Accessories</li>\n            <li>/</li>\n            <li>Wallets</li>\n            <li>/</li>\n            <li>Premium Leather Wallet</li>\n            <li>/</li>\n            <li className=\"font-semibold text-gray-900\">Brown Classic</li>\n          </ol>\n        </nav>\n\n        <div className=\"bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6\">\n          <h2 className=\"font-semibold text-yellow-900 mb-2\">\n            Multi-Page Variant Example\n          </h2>\n          <p className=\"text-sm text-yellow-800\">\n            This page demonstrates the multi-page approach where each product\n            variant has its own page. The variant references its parent\n            ProductGroup using the <code>isVariantOf</code> property. In a real\n            implementation, each color/pattern combination would have its own\n            URL and page.\n          </p>\n        </div>\n\n        <div className=\"grid md:grid-cols-2 gap-8\">\n          <div className=\"space-y-4\">\n            <div className=\"bg-gray-200 rounded-lg aspect-square flex items-center justify-center\">\n              <span className=\"text-gray-500\">Brown Classic Wallet Image</span>\n            </div>\n            <div className=\"grid grid-cols-4 gap-2\">\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n              <div className=\"bg-gray-200 rounded aspect-square\"></div>\n            </div>\n          </div>\n\n          <div className=\"space-y-6\">\n            <div>\n              <h1 className=\"text-3xl font-bold mb-2\">\n                Premium Leather Wallet - Brown Classic\n              </h1>\n              <div className=\"flex items-center gap-4 mb-4\">\n                <div className=\"flex text-yellow-400\">{\"★★★★★\"}</div>\n                <span className=\"text-gray-600\">4.7 out of 5 (89 reviews)</span>\n              </div>\n              <p className=\"text-gray-600\">by Craftsman Leather Co.</p>\n            </div>\n\n            <div>\n              <p className=\"text-gray-700\">\n                Handcrafted genuine leather wallet in classic brown. Features\n                multiple card slots, bill compartment, and RFID blocking\n                technology. Each wallet is carefully crafted by skilled artisans\n                ensuring premium quality and durability.\n              </p>\n            </div>\n\n            <div className=\"space-y-4\">\n              <div>\n                <label className=\"block text-sm font-medium mb-2\">\n                  Color & Pattern\n                </label>\n                <div className=\"grid grid-cols-3 gap-2\">\n                  <a\n                    href=\"#\"\n                    className=\"border-2 border-blue-500 rounded p-2 text-center\"\n                  >\n                    <div className=\"w-full h-12 bg-amber-700 rounded mb-1\"></div>\n                    <span className=\"text-xs\">Brown Classic</span>\n                  </a>\n                  <a\n                    href=\"/products/leather-wallet/black-modern\"\n                    className=\"border rounded p-2 text-center hover:border-gray-400\"\n                  >\n                    <div className=\"w-full h-12 bg-black rounded mb-1\"></div>\n                    <span className=\"text-xs\">Black Modern</span>\n                  </a>\n                  <a\n                    href=\"/products/leather-wallet/tan-vintage\"\n                    className=\"border rounded p-2 text-center hover:border-gray-400\"\n                  >\n                    <div className=\"w-full h-12 bg-orange-300 rounded mb-1\"></div>\n                    <span className=\"text-xs\">Tan Vintage</span>\n                  </a>\n                </div>\n                <p className=\"text-xs text-gray-500 mt-2\">\n                  Click to view other color/pattern combinations\n                </p>\n              </div>\n            </div>\n\n            <div className=\"space-y-2\">\n              <div className=\"flex items-baseline gap-2\">\n                <span className=\"text-3xl font-bold\">$79.99</span>\n                <span className=\"text-sm text-gray-500\">USD</span>\n              </div>\n              <p className=\"text-green-600 font-medium\">✓ In Stock</p>\n              <div className=\"text-sm text-gray-500 space-y-1\">\n                <p>SKU: LW2024-BRN-CLS</p>\n                <p>GTIN: 98766051105555</p>\n                <p>Product Group: LW2024</p>\n              </div>\n            </div>\n\n            <div className=\"space-y-3\">\n              <button className=\"w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700\">\n                Add to Cart - Brown Classic\n              </button>\n              <button className=\"w-full border border-gray-300 py-3 px-6 rounded-lg font-medium hover:bg-gray-50\">\n                View All Color Options\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Other Available Variants</h2>\n          <div className=\"grid md:grid-cols-3 gap-4\">\n            <a\n              href=\"/products/leather-wallet/brown-classic\"\n              className=\"border-2 border-blue-500 rounded-lg p-4 bg-blue-50\"\n            >\n              <div className=\"bg-amber-700 rounded aspect-square mb-3\"></div>\n              <h3 className=\"font-semibold\">Brown Classic</h3>\n              <p className=\"text-gray-600\">$79.99</p>\n              <p className=\"text-sm text-blue-600\">Currently viewing</p>\n            </a>\n            <a\n              href=\"/products/leather-wallet/black-modern\"\n              className=\"border rounded-lg p-4 hover:border-gray-400\"\n            >\n              <div className=\"bg-black rounded aspect-square mb-3\"></div>\n              <h3 className=\"font-semibold\">Black Modern</h3>\n              <p className=\"text-gray-600\">$89.99</p>\n              <p className=\"text-green-600 text-sm\">✓ In Stock</p>\n            </a>\n            <a\n              href=\"/products/leather-wallet/tan-vintage\"\n              className=\"border rounded-lg p-4 hover:border-gray-400\"\n            >\n              <div className=\"bg-orange-300 rounded aspect-square mb-3\"></div>\n              <h3 className=\"font-semibold\">Tan Vintage</h3>\n              <p className=\"text-gray-600\">$94.99</p>\n              <p className=\"text-green-600 text-sm\">✓ In Stock</p>\n            </a>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8\">\n          <h2 className=\"text-2xl font-bold mb-6\">Product Details</h2>\n          <div className=\"grid md:grid-cols-2 gap-8\">\n            <div>\n              <h3 className=\"font-semibold mb-3\">Features</h3>\n              <ul className=\"space-y-2 text-gray-700\">\n                <li>• 8 card slots</li>\n                <li>• 2 bill compartments</li>\n                <li>• RFID blocking technology</li>\n                <li>• Genuine leather construction</li>\n                <li>• Hand-stitched edges</li>\n                <li>• Gift box included</li>\n              </ul>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-3\">Specifications</h3>\n              <dl className=\"space-y-2 text-sm\">\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Dimensions:</dt>\n                  <dd>4.5\" x 3.5\" x 0.5\"</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Material:</dt>\n                  <dd>Genuine Leather</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Color:</dt>\n                  <dd>Brown</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Pattern:</dt>\n                  <dd>Classic</dd>\n                </div>\n                <div className=\"flex justify-between\">\n                  <dt className=\"text-gray-600\">Warranty:</dt>\n                  <dd>2 years</dd>\n                </div>\n              </dl>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-12 border-t pt-8 bg-gray-50 p-6 rounded-lg\">\n          <h3 className=\"font-semibold mb-3\">\n            Structured Data Implementation Note\n          </h3>\n          <div className=\"text-sm text-gray-700 space-y-2\">\n            <p>\n              This page demonstrates the{\" \"}\n              <strong>multi-page variant approach</strong> where:\n            </p>\n            <ul className=\"list-disc list-inside space-y-1 ml-4\">\n              <li>Each variant has its own dedicated page/URL</li>\n              <li>\n                The variant Product includes <code>isVariantOf</code> to\n                reference the ProductGroup\n              </li>\n              <li>\n                The ProductGroup definition is duplicated on each variant page\n              </li>\n              <li>\n                The ProductGroup lists all variants, with URL-only references to\n                other pages\n              </li>\n            </ul>\n            <p className=\"mt-3\">\n              This approach is ideal for e-commerce sites where each variant\n              needs its own SEO-optimized page.\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/product-with-return-policy/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function ProductWithReturnPolicyPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProductJsonLd\n        name=\"Premium Wireless Headphones\"\n        description=\"High-quality wireless headphones with active noise cancellation and 30-hour battery life\"\n        image={[\n          \"https://example.com/headphones-1.jpg\",\n          \"https://example.com/headphones-2.jpg\",\n          \"https://example.com/headphones-3.jpg\",\n        ]}\n        sku=\"WH-1000XM5\"\n        mpn=\"WH1000XM5B\"\n        brand=\"AudioTech\"\n        offers={{\n          price: 349.99,\n          priceCurrency: \"USD\",\n          availability: \"https://schema.org/InStock\",\n          priceValidUntil: \"2025-12-31\",\n          seller: {\n            name: \"TechStore Online\",\n            url: \"https://www.techstore.com\",\n          },\n          hasMerchantReturnPolicy: {\n            applicableCountry: \"US\",\n            returnPolicyCategory:\n              \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n            merchantReturnDays: 45,\n            itemCondition: [\n              \"https://schema.org/NewCondition\",\n              \"https://schema.org/DamagedCondition\",\n            ],\n            returnMethod: [\n              \"https://schema.org/ReturnByMail\",\n              \"https://schema.org/ReturnInStore\",\n            ],\n            returnFees: \"https://schema.org/FreeReturn\",\n            refundType: [\n              \"https://schema.org/FullRefund\",\n              \"https://schema.org/ExchangeRefund\",\n            ],\n            returnLabelSource: \"https://schema.org/ReturnLabelDownloadAndPrint\",\n            customerRemorseReturnFees: \"https://schema.org/FreeReturn\",\n            itemDefectReturnFees: \"https://schema.org/FreeReturn\",\n          },\n        }}\n        aggregateRating={{\n          ratingValue: 4.8,\n          ratingCount: 2847,\n          reviewCount: 1562,\n        }}\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <div className=\"grid md:grid-cols-2 gap-8 mb-8\">\n          <div>\n            <h1 className=\"text-3xl font-bold mb-2\">\n              Premium Wireless Headphones\n            </h1>\n            <div className=\"flex items-center mb-4\">\n              <span className=\"text-2xl font-semibold text-green-600\">\n                $349.99\n              </span>\n              <span className=\"ml-4 text-sm text-gray-600\">\n                SKU: WH-1000XM5\n              </span>\n            </div>\n            <div className=\"flex items-center mb-4\">\n              <div className=\"flex text-yellow-400\">\n                {\"★★★★★\".split(\"\").map((star, i) => (\n                  <span key={i}>{star}</span>\n                ))}\n              </div>\n              <span className=\"ml-2 text-sm text-gray-600\">\n                4.8 (2,847 reviews)\n              </span>\n            </div>\n            <p className=\"text-gray-700 mb-6\">\n              High-quality wireless headphones with active noise cancellation\n              and 30-hour battery life. Experience premium sound quality with\n              industry-leading noise cancellation technology.\n            </p>\n            <button className=\"w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors\">\n              Add to Cart\n            </button>\n          </div>\n        </div>\n\n        <section className=\"mb-8 bg-gray-50 p-6 rounded-lg\">\n          <h2 className=\"text-2xl font-semibold mb-4\">\n            Extended 45-Day Return Policy\n          </h2>\n          <div className=\"grid md:grid-cols-2 gap-6\">\n            <div>\n              <h3 className=\"font-semibold mb-2\">Return Window</h3>\n              <p className=\"text-gray-700\">45 days from delivery date</p>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-2\">Return Methods</h3>\n              <p className=\"text-gray-700\">By mail or in-store</p>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-2\">Return Shipping</h3>\n              <p className=\"text-gray-700\">Free returns on all orders</p>\n            </div>\n            <div>\n              <h3 className=\"font-semibold mb-2\">Refund Options</h3>\n              <p className=\"text-gray-700\">Full refund or exchange</p>\n            </div>\n          </div>\n          <div className=\"mt-4 pt-4 border-t\">\n            <p className=\"text-sm text-gray-600\">\n              We accept returns for new items in original packaging or defective\n              products. Download and print your return label from your account.\n            </p>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Key Features</h2>\n          <ul className=\"space-y-2 text-gray-700\">\n            <li>✓ Industry-leading noise cancellation</li>\n            <li>✓ 30-hour battery life</li>\n            <li>✓ Premium sound quality</li>\n            <li>✓ Comfortable all-day wear</li>\n            <li>✓ Multi-device connectivity</li>\n            <li>✓ Quick charge capability</li>\n          </ul>\n        </section>\n\n        <section className=\"text-sm text-gray-600\">\n          <p>\n            This example demonstrates a product with a specific return policy\n            that may differ from the store's general return policy. The 45-day\n            return window and free returns make this product especially\n            attractive to customers.\n          </p>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/profile/page.tsx",
    "content": "import { ProfilePageJsonLd } from \"next-seo\";\n\nexport default function ProfilePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProfilePageJsonLd\n        mainEntity=\"Angelo Huff\"\n        dateCreated=\"2024-12-23T12:34:00-05:00\"\n        dateModified=\"2024-12-26T14:53:00-05:00\"\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <div className=\"bg-white rounded-lg shadow-md p-8\">\n          <div className=\"flex items-center mb-6\">\n            <div className=\"w-24 h-24 bg-gray-300 rounded-full mr-6\"></div>\n            <div>\n              <h1 className=\"text-3xl font-bold\">Angelo Huff</h1>\n              <p className=\"text-gray-600\">Member since December 23, 2024</p>\n            </div>\n          </div>\n\n          <div className=\"mb-6\">\n            <p className=\"text-gray-700\">\n              Welcome to my profile! I'm passionate about technology and sharing\n              knowledge.\n            </p>\n          </div>\n\n          <div className=\"grid grid-cols-2 gap-4 mb-6\">\n            <div className=\"bg-gray-100 p-4 rounded\">\n              <h3 className=\"font-semibold text-gray-700\">Followers</h3>\n              <p className=\"text-2xl font-bold\">1</p>\n            </div>\n            <div className=\"bg-gray-100 p-4 rounded\">\n              <h3 className=\"font-semibold text-gray-700\">Posts</h3>\n              <p className=\"text-2xl font-bold\">0</p>\n            </div>\n          </div>\n\n          <div>\n            <h2 className=\"text-xl font-semibold mb-4\">Recent Activity</h2>\n            <p className=\"text-gray-600\">No recent activity to show.</p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/profile-advanced/page.tsx",
    "content": "import { ProfilePageJsonLd } from \"next-seo\";\n\nexport default function AdvancedProfilePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProfilePageJsonLd\n        mainEntity={{\n          name: \"Angelo Huff\",\n          alternateName: \"ahuff23\",\n          identifier: \"123475623\",\n          description: \"Defender of Truth\",\n          image: \"https://example.com/avatars/ahuff23.jpg\",\n          sameAs: [\n            \"https://www.example.com/real-angelo\",\n            \"https://example.com/profile/therealangelohuff\",\n          ],\n          interactionStatistic: [\n            {\n              interactionType: \"https://schema.org/FollowAction\",\n              userInteractionCount: 1,\n            },\n            {\n              interactionType: \"https://schema.org/LikeAction\",\n              userInteractionCount: 5,\n            },\n          ],\n          agentInteractionStatistic: {\n            interactionType: \"https://schema.org/WriteAction\",\n            userInteractionCount: 2346,\n          },\n        }}\n        dateCreated=\"2024-12-23T12:34:00-05:00\"\n        dateModified=\"2024-12-26T14:53:00-05:00\"\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <div className=\"bg-white rounded-lg shadow-md p-8\">\n          <div className=\"flex items-center mb-6\">\n            <div>\n              <h1 className=\"text-3xl font-bold\">Angelo Huff</h1>\n              <p className=\"text-xl text-gray-600\">@ahuff23</p>\n              <p className=\"text-gray-700 mt-1\">Defender of Truth</p>\n              <p className=\"text-sm text-gray-500 mt-2\">ID: 123475623</p>\n            </div>\n          </div>\n\n          <div className=\"mb-6\">\n            <h2 className=\"text-xl font-semibold mb-2\">About</h2>\n            <p className=\"text-gray-700\">\n              I'm a passionate advocate for truth and transparency in the\n              digital age. With years of experience in fact-checking and digital\n              forensics, I help communities identify and combat misinformation.\n            </p>\n          </div>\n\n          <div className=\"grid grid-cols-3 gap-4 mb-6\">\n            <div className=\"bg-blue-50 p-4 rounded text-center\">\n              <h3 className=\"font-semibold text-gray-700\">Followers</h3>\n              <p className=\"text-3xl font-bold text-blue-600\">1</p>\n            </div>\n            <div className=\"bg-green-50 p-4 rounded text-center\">\n              <h3 className=\"font-semibold text-gray-700\">Likes Given</h3>\n              <p className=\"text-3xl font-bold text-green-600\">5</p>\n            </div>\n            <div className=\"bg-purple-50 p-4 rounded text-center\">\n              <h3 className=\"font-semibold text-gray-700\">Posts Written</h3>\n              <p className=\"text-3xl font-bold text-purple-600\">2,346</p>\n            </div>\n          </div>\n\n          <div className=\"mb-6\">\n            <h2 className=\"text-xl font-semibold mb-4\">External Profiles</h2>\n            <div className=\"space-y-2\">\n              <a\n                href=\"https://www.example.com/real-angelo\"\n                className=\"block text-blue-600 hover:underline\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                Personal Website\n              </a>\n              <a\n                href=\"https://example.com/profile/therealangelohuff\"\n                className=\"block text-blue-600 hover:underline\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                Professional Profile\n              </a>\n            </div>\n          </div>\n\n          <div className=\"mb-6\">\n            <h2 className=\"text-xl font-semibold mb-4\">Recent Posts</h2>\n            <div className=\"space-y-4\">\n              <div className=\"border-l-4 border-purple-600 pl-4\">\n                <h3 className=\"font-semibold\">How to Verify Sources Online</h3>\n                <p className=\"text-gray-600 text-sm\">Posted 2 days ago</p>\n                <p className=\"text-gray-700 mt-1\">\n                  A comprehensive guide to fact-checking and source\n                  verification...\n                </p>\n              </div>\n              <div className=\"border-l-4 border-purple-600 pl-4\">\n                <h3 className=\"font-semibold\">\n                  The Importance of Digital Literacy\n                </h3>\n                <p className=\"text-gray-600 text-sm\">Posted 1 week ago</p>\n                <p className=\"text-gray-700 mt-1\">\n                  In today's interconnected world, understanding how to\n                  navigate...\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"text-sm text-gray-500\">\n            <p>Profile created: December 23, 2024</p>\n            <p>Last updated: December 26, 2024</p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/profile-organization/page.tsx",
    "content": "import { ProfilePageJsonLd } from \"next-seo\";\n\nexport default function OrganizationProfilePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ProfilePageJsonLd\n        mainEntity={{\n          \"@type\": \"Organization\",\n          name: \"TechForum Community\",\n          url: \"https://techforum.example.com\",\n          logo: \"https://techforum.example.com/logo.png\",\n          alternateName: \"TechForum\",\n          identifier: \"org-789012\",\n          description: \"A vibrant community for technology enthusiasts\",\n          sameAs: [\n            \"https://twitter.com/techforum\",\n            \"https://linkedin.com/company/techforum\",\n            \"https://github.com/techforum\",\n          ],\n          interactionStatistic: [\n            {\n              interactionType: \"https://schema.org/FollowAction\",\n              userInteractionCount: 15000,\n            },\n            {\n              interactionType: \"https://schema.org/LikeAction\",\n              userInteractionCount: 45000,\n            },\n          ],\n          agentInteractionStatistic: {\n            interactionType: \"https://schema.org/WriteAction\",\n            userInteractionCount: 8500,\n          },\n        }}\n        dateCreated=\"2020-01-15T09:00:00-05:00\"\n        dateModified=\"2024-12-26T16:30:00-05:00\"\n      />\n\n      <div className=\"max-w-4xl mx-auto\">\n        <div className=\"bg-white rounded-lg shadow-md p-8\">\n          <div className=\"flex items-center mb-6\">\n            <div>\n              <h1 className=\"text-3xl font-bold\">TechForum Community</h1>\n              <p className=\"text-xl text-gray-600\">@TechForum</p>\n              <p className=\"text-gray-700 mt-1\">\n                A vibrant community for technology enthusiasts\n              </p>\n              <a\n                href=\"https://techforum.example.com\"\n                className=\"text-blue-600 hover:underline text-sm mt-1 inline-block\"\n              >\n                techforum.example.com\n              </a>\n            </div>\n          </div>\n\n          <div className=\"mb-6\">\n            <h2 className=\"text-xl font-semibold mb-2\">About</h2>\n            <p className=\"text-gray-700\">\n              TechForum Community is a leading online platform where technology\n              enthusiasts, professionals, and learners come together to share\n              knowledge, discuss latest trends, and collaborate on innovative\n              projects. Founded in 2020, we've grown to become one of the most\n              active tech communities online.\n            </p>\n          </div>\n\n          <div className=\"grid grid-cols-3 gap-4 mb-6\">\n            <div className=\"bg-blue-50 p-4 rounded text-center\">\n              <h3 className=\"font-semibold text-gray-700\">Followers</h3>\n              <p className=\"text-3xl font-bold text-blue-600\">15K</p>\n            </div>\n            <div className=\"bg-green-50 p-4 rounded text-center\">\n              <h3 className=\"font-semibold text-gray-700\">Total Likes</h3>\n              <p className=\"text-3xl font-bold text-green-600\">45K</p>\n            </div>\n            <div className=\"bg-purple-50 p-4 rounded text-center\">\n              <h3 className=\"font-semibold text-gray-700\">Posts</h3>\n              <p className=\"text-3xl font-bold text-purple-600\">8.5K</p>\n            </div>\n          </div>\n\n          <div className=\"mb-6\">\n            <h2 className=\"text-xl font-semibold mb-4\">Connect With Us</h2>\n            <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n              <a\n                href=\"https://twitter.com/techforum\"\n                className=\"flex items-center p-3 border rounded hover:bg-gray-50\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                <span className=\"text-blue-400 mr-2\">🐦</span>\n                Twitter\n              </a>\n              <a\n                href=\"https://linkedin.com/company/techforum\"\n                className=\"flex items-center p-3 border rounded hover:bg-gray-50\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                <span className=\"text-blue-700 mr-2\">💼</span>\n                LinkedIn\n              </a>\n              <a\n                href=\"https://github.com/techforum\"\n                className=\"flex items-center p-3 border rounded hover:bg-gray-50\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                <span className=\"mr-2\">🐙</span>\n                GitHub\n              </a>\n            </div>\n          </div>\n\n          <div className=\"mb-6\">\n            <h2 className=\"text-xl font-semibold mb-4\">Recent Updates</h2>\n            <div className=\"space-y-4\">\n              <div className=\"border-l-4 border-blue-600 pl-4\">\n                <h3 className=\"font-semibold\">\n                  New AI & Machine Learning Section\n                </h3>\n                <p className=\"text-gray-600 text-sm\">Posted 3 days ago</p>\n                <p className=\"text-gray-700 mt-1\">\n                  We've launched a dedicated section for AI and ML\n                  discussions...\n                </p>\n              </div>\n              <div className=\"border-l-4 border-blue-600 pl-4\">\n                <h3 className=\"font-semibold\">\n                  Community Hackathon 2025 Announced\n                </h3>\n                <p className=\"text-gray-600 text-sm\">Posted 1 week ago</p>\n                <p className=\"text-gray-700 mt-1\">\n                  Join us for our annual hackathon event this March...\n                </p>\n              </div>\n              <div className=\"border-l-4 border-blue-600 pl-4\">\n                <h3 className=\"font-semibold\">15,000 Members Milestone!</h3>\n                <p className=\"text-gray-600 text-sm\">Posted 2 weeks ago</p>\n                <p className=\"text-gray-700 mt-1\">\n                  We're thrilled to announce that our community has reached...\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"text-sm text-gray-500\">\n            <p>Organization ID: org-789012</p>\n            <p>Founded: January 15, 2020</p>\n            <p>Last updated: December 26, 2024</p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/quiz/page.tsx",
    "content": "import { QuizJsonLd } from \"next-seo\";\n\nexport default function QuizPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <QuizJsonLd\n        questions={[\n          {\n            question: \"What is the capital of France?\",\n            answer: \"Paris\",\n          },\n          {\n            question: \"What is 2 + 2?\",\n            answer: \"4\",\n          },\n          {\n            question: \"Who wrote Romeo and Juliet?\",\n            answer: \"William Shakespeare\",\n          },\n        ]}\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <h1>General Knowledge Quiz</h1>\n\n        <div className=\"space-y-6\">\n          <div className=\"border p-4 rounded\">\n            <p className=\"font-semibold\">Q: What is the capital of France?</p>\n            <p className=\"mt-2\">A: Paris</p>\n          </div>\n\n          <div className=\"border p-4 rounded\">\n            <p className=\"font-semibold\">Q: What is 2 + 2?</p>\n            <p className=\"mt-2\">A: 4</p>\n          </div>\n\n          <div className=\"border p-4 rounded\">\n            <p className=\"font-semibold\">Q: Who wrote Romeo and Juliet?</p>\n            <p className=\"mt-2\">A: William Shakespeare</p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/quiz-advanced/page.tsx",
    "content": "import { QuizJsonLd } from \"next-seo\";\n\nexport default function AdvancedQuizPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <QuizJsonLd\n        questions={[\n          // String format - simple flashcard\n          \"The Earth revolves around the Sun in approximately 365.25 days\",\n          // Question/answer format\n          {\n            question: \"What is the chemical formula for water?\",\n            answer: \"H2O\",\n          },\n          // Text/acceptedAnswer with string\n          {\n            text: \"What causes tides on Earth?\",\n            acceptedAnswer: \"The gravitational pull of the Moon and Sun\",\n          },\n          // Text/acceptedAnswer with Answer object\n          {\n            text: \"Explain the greenhouse effect\",\n            acceptedAnswer: {\n              \"@type\": \"Answer\",\n              text: \"The greenhouse effect is a natural process where certain gases in Earth's atmosphere trap heat from the sun, warming the planet's surface\",\n            },\n          },\n        ]}\n        about={{\n          name: \"Earth and Space Science\",\n          description:\n            \"Fundamental concepts about Earth, space, and environmental systems\",\n          url: \"https://example.com/earth-science\",\n        }}\n        educationalAlignment={[\n          {\n            type: \"educationalSubject\",\n            name: \"Earth Science\",\n          },\n          {\n            type: \"educationalLevel\",\n            name: \"High School\",\n          },\n        ]}\n        scriptId=\"advanced-quiz\"\n        scriptKey=\"earth-science-quiz\"\n      />\n\n      <div className=\"prose lg:prose-xl max-w-4xl\">\n        <h1>Earth and Space Science Quiz</h1>\n        <p className=\"text-lg text-gray-600\">\n          Advanced flashcard quiz demonstrating all QuizJsonLd features\n        </p>\n\n        <div className=\"mt-4 p-4 bg-blue-50 rounded-lg\">\n          <p>\n            <strong>Subject:</strong> Earth Science\n          </p>\n          <p>\n            <strong>Level:</strong> High School\n          </p>\n          <p>\n            <strong>Topic:</strong> Fundamental concepts about Earth, space, and\n            environmental systems\n          </p>\n        </div>\n\n        <h2 className=\"mt-8\">Flashcards</h2>\n\n        <div className=\"space-y-6\">\n          <div className=\"border-2 border-blue-200 rounded-lg overflow-hidden\">\n            <div className=\"bg-blue-100 p-4\">\n              <p className=\"font-semibold\">Fact Card</p>\n            </div>\n            <div className=\"p-4\">\n              <p>\n                The Earth revolves around the Sun in approximately 365.25 days\n              </p>\n            </div>\n          </div>\n\n          <div className=\"border-2 border-green-200 rounded-lg overflow-hidden\">\n            <div className=\"bg-green-100 p-4\">\n              <p className=\"font-semibold\">\n                Q: What is the chemical formula for water?\n              </p>\n            </div>\n            <div className=\"p-4 bg-green-50\">\n              <p className=\"text-green-700\">A: H2O</p>\n            </div>\n          </div>\n\n          <div className=\"border-2 border-purple-200 rounded-lg overflow-hidden\">\n            <div className=\"bg-purple-100 p-4\">\n              <p className=\"font-semibold\">Q: What causes tides on Earth?</p>\n            </div>\n            <div className=\"p-4 bg-purple-50\">\n              <p className=\"text-purple-700\">\n                A: The gravitational pull of the Moon and Sun\n              </p>\n            </div>\n          </div>\n\n          <div className=\"border-2 border-orange-200 rounded-lg overflow-hidden\">\n            <div className=\"bg-orange-100 p-4\">\n              <p className=\"font-semibold\">Q: Explain the greenhouse effect</p>\n            </div>\n            <div className=\"p-4 bg-orange-50\">\n              <p className=\"text-orange-700\">\n                A: The greenhouse effect is a natural process where certain\n                gases in Earth's atmosphere trap heat from the sun, warming the\n                planet's surface\n              </p>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-8 p-4 bg-gray-100 rounded\">\n          <p className=\"text-sm text-gray-600\">\n            This page demonstrates all features of the QuizJsonLd component\n            including: multiple question formats, about property with full Thing\n            object, educational alignment for subject and level, and custom\n            script IDs.\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/quiz-biology/page.tsx",
    "content": "import { QuizJsonLd } from \"next-seo\";\n\nexport default function BiologyQuizPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <QuizJsonLd\n        questions={[\n          {\n            question: \"What is the powerhouse of the cell?\",\n            answer: \"Mitochondria\",\n          },\n          {\n            question:\n              \"What process do plants use to convert sunlight into energy?\",\n            answer: \"Photosynthesis\",\n          },\n          {\n            question: \"What is DNA?\",\n            answer:\n              \"Deoxyribonucleic acid - the molecule that carries genetic information\",\n          },\n          {\n            question: \"What are the building blocks of proteins?\",\n            answer: \"Amino acids\",\n          },\n        ]}\n        about=\"Cell Biology\"\n        educationalAlignment={[\n          {\n            type: \"educationalSubject\",\n            name: \"Biology\",\n          },\n          {\n            type: \"educationalLevel\",\n            name: \"Grade 10\",\n          },\n        ]}\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <h1>Biology Quiz: Cell Structure and Function</h1>\n        <p className=\"text-lg\">Subject: Biology | Level: Grade 10</p>\n\n        <div className=\"space-y-6 mt-8\">\n          <div className=\"border p-4 rounded bg-green-50\">\n            <p className=\"font-semibold\">\n              Q: What is the powerhouse of the cell?\n            </p>\n            <p className=\"mt-2 text-green-700\">A: Mitochondria</p>\n          </div>\n\n          <div className=\"border p-4 rounded bg-green-50\">\n            <p className=\"font-semibold\">\n              Q: What process do plants use to convert sunlight into energy?\n            </p>\n            <p className=\"mt-2 text-green-700\">A: Photosynthesis</p>\n          </div>\n\n          <div className=\"border p-4 rounded bg-green-50\">\n            <p className=\"font-semibold\">Q: What is DNA?</p>\n            <p className=\"mt-2 text-green-700\">\n              A: Deoxyribonucleic acid - the molecule that carries genetic\n              information\n            </p>\n          </div>\n\n          <div className=\"border p-4 rounded bg-green-50\">\n            <p className=\"font-semibold\">\n              Q: What are the building blocks of proteins?\n            </p>\n            <p className=\"mt-2 text-green-700\">A: Amino acids</p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/recipe/page.tsx",
    "content": "import { RecipeJsonLd } from \"next-seo\";\n\nexport default function RecipePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <RecipeJsonLd\n        name=\"Classic Chocolate Chip Cookies\"\n        image=\"https://example.com/images/chocolate-chip-cookies.jpg\"\n        description=\"The perfect chocolate chip cookies - crispy edges with soft, chewy centers\"\n        author=\"Sarah Baker\"\n        datePublished=\"2024-01-20T09:00:00+00:00\"\n        url=\"https://example.com/recipes/chocolate-chip-cookies\"\n        prepTime=\"PT20M\"\n        cookTime=\"PT12M\"\n        totalTime=\"PT32M\"\n        recipeYield=\"36 cookies\"\n        recipeCategory=\"dessert\"\n        recipeCuisine=\"American\"\n        keywords=\"cookies, chocolate chip, dessert, baking\"\n        recipeIngredient={[\n          \"2 1/4 cups all-purpose flour\",\n          \"1 teaspoon baking soda\",\n          \"1 teaspoon salt\",\n          \"1 cup (2 sticks) butter, softened\",\n          \"3/4 cup granulated sugar\",\n          \"3/4 cup packed brown sugar\",\n          \"1 teaspoon vanilla extract\",\n          \"2 large eggs\",\n          \"2 cups chocolate chips\",\n        ]}\n        recipeInstructions={[\n          \"Preheat oven to 375°F\",\n          \"Combine flour, baking soda and salt in small bowl\",\n          \"Beat butter, granulated sugar, brown sugar and vanilla extract in large mixer bowl until creamy\",\n          \"Add eggs, one at a time, beating well after each addition\",\n          \"Gradually beat in flour mixture\",\n          \"Stir in chocolate chips\",\n          \"Drop by rounded tablespoon onto ungreased baking sheets\",\n          \"Bake for 9 to 11 minutes or until golden brown\",\n          \"Cool on baking sheets for 2 minutes; remove to wire racks to cool completely\",\n        ]}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Classic Chocolate Chip Cookies</h1>\n        <p className=\"text-gray-600\">By Sarah Baker | January 20, 2024</p>\n\n        <p>\n          There's nothing quite like a warm, homemade chocolate chip cookie\n          fresh from the oven. This recipe produces cookies with perfectly\n          crispy edges and soft, chewy centers.\n        </p>\n\n        <h2>Ingredients</h2>\n        <ul>\n          <li>2 1/4 cups all-purpose flour</li>\n          <li>1 teaspoon baking soda</li>\n          <li>1 teaspoon salt</li>\n          <li>1 cup (2 sticks) butter, softened</li>\n          <li>3/4 cup granulated sugar</li>\n          <li>3/4 cup packed brown sugar</li>\n          <li>1 teaspoon vanilla extract</li>\n          <li>2 large eggs</li>\n          <li>2 cups chocolate chips</li>\n        </ul>\n\n        <h2>Instructions</h2>\n        <ol>\n          <li>Preheat oven to 375°F</li>\n          <li>Combine flour, baking soda and salt in small bowl</li>\n          <li>\n            Beat butter, granulated sugar, brown sugar and vanilla extract until\n            creamy\n          </li>\n          <li>Add eggs, one at a time, beating well after each addition</li>\n          <li>Gradually beat in flour mixture</li>\n          <li>Stir in chocolate chips</li>\n          <li>Drop by rounded tablespoon onto ungreased baking sheets</li>\n          <li>Bake for 9 to 11 minutes or until golden brown</li>\n          <li>Cool on baking sheets for 2 minutes; remove to wire racks</li>\n        </ol>\n\n        <div className=\"bg-gray-100 p-4 rounded-lg my-8\">\n          <h3>Recipe Notes</h3>\n          <p>\n            For chewier cookies, slightly underbake them. For crispier cookies,\n            bake for an additional 1-2 minutes until edges are deep golden\n            brown.\n          </p>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/recipe-advanced/page.tsx",
    "content": "import { RecipeJsonLd } from \"next-seo\";\n\nexport default function RecipeAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <RecipeJsonLd\n        name=\"Authentic Italian Tiramisu\"\n        image={[\n          \"https://example.com/images/tiramisu-16x9.jpg\",\n          \"https://example.com/images/tiramisu-4x3.jpg\",\n          \"https://example.com/images/tiramisu-1x1.jpg\",\n        ]}\n        description=\"An authentic Italian tiramisu recipe with layers of coffee-soaked ladyfingers, mascarpone cream, and cocoa powder\"\n        author={{\n          \"@type\": \"Organization\",\n          name: \"La Cucina Italiana\",\n          url: \"https://example.com\",\n          logo: {\n            \"@type\": \"ImageObject\",\n            url: \"https://example.com/logo.png\",\n            width: 200,\n            height: 200,\n          },\n        }}\n        datePublished=\"2024-01-25T14:00:00+00:00\"\n        url=\"https://example.com/recipes/authentic-italian-tiramisu\"\n        prepTime=\"PT30M\"\n        cookTime=\"PT0M\"\n        totalTime=\"PT4H30M\"\n        recipeYield=\"8 servings\"\n        recipeCategory=\"dessert\"\n        recipeCuisine=\"Italian\"\n        keywords=\"tiramisu, italian dessert, mascarpone, coffee, ladyfingers\"\n        recipeIngredient={[\n          \"6 egg yolks\",\n          \"3/4 cup sugar\",\n          \"1 1/3 cups mascarpone cheese (room temperature)\",\n          \"1 3/4 cups heavy cream\",\n          \"2 cups strong espresso (cooled)\",\n          \"1/2 cup coffee liqueur\",\n          \"2 packages (7 oz each) ladyfinger cookies\",\n          \"Unsweetened cocoa powder for dusting\",\n          \"Dark chocolate shavings for garnish\",\n        ]}\n        recipeInstructions={[\n          {\n            \"@type\": \"HowToSection\",\n            name: \"Prepare the Mascarpone Cream\",\n            itemListElement: [\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Whisk egg yolks and sugar in a double boiler over simmering water until thick and pale (about 5 minutes)\",\n                image: \"https://example.com/images/tiramisu-step1.jpg\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Remove from heat and whisk in mascarpone until smooth\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                text: \"In a separate bowl, whip cream to stiff peaks\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Gently fold the whipped cream into mascarpone mixture\",\n              },\n            ],\n          },\n          {\n            \"@type\": \"HowToSection\",\n            name: \"Assemble the Tiramisu\",\n            itemListElement: [\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Combine espresso and coffee liqueur in a shallow dish\",\n                name: \"Prepare coffee mixture\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Quickly dip each ladyfinger into coffee mixture and arrange in a 9x13 inch dish\",\n                name: \"First layer\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Spread half of mascarpone mixture over ladyfingers\",\n                name: \"Add cream\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Repeat layers with remaining ladyfingers and mascarpone mixture\",\n                name: \"Second layer\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Cover and refrigerate for at least 4 hours, preferably overnight\",\n                name: \"Chill\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Before serving, dust with cocoa powder and garnish with chocolate shavings\",\n                name: \"Finish\",\n              },\n            ],\n          },\n        ]}\n        nutrition={{\n          \"@type\": \"NutritionInformation\",\n          calories: \"385 calories\",\n          proteinContent: \"7g\",\n          carbohydrateContent: \"28g\",\n          fatContent: \"28g\",\n          saturatedFatContent: \"16g\",\n          cholesterolContent: \"215mg\",\n          sodiumContent: \"95mg\",\n          sugarContent: \"20g\",\n          servingSize: \"1 piece (1/8 of dish)\",\n        }}\n        aggregateRating={{\n          \"@type\": \"AggregateRating\",\n          ratingValue: 4.9,\n          ratingCount: 487,\n          reviewCount: 423,\n          bestRating: 5,\n          worstRating: 1,\n        }}\n        video={{\n          \"@type\": \"VideoObject\",\n          name: \"How to Make Authentic Tiramisu\",\n          description:\n            \"Watch our Italian chef demonstrate the traditional method of making tiramisu\",\n          thumbnailUrl: [\n            \"https://example.com/video/tiramisu-thumb-1.jpg\",\n            \"https://example.com/video/tiramisu-thumb-2.jpg\",\n          ],\n          contentUrl: \"https://example.com/videos/tiramisu-tutorial.mp4\",\n          embedUrl: \"https://example.com/embed/tiramisu-tutorial\",\n          uploadDate: \"2024-01-20T10:00:00+00:00\",\n          duration: \"PT12M45S\",\n        }}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>Authentic Italian Tiramisu</h1>\n        <p className=\"text-gray-600\">\n          By La Cucina Italiana | January 25, 2024\n        </p>\n\n        <div className=\"bg-yellow-50 border-l-4 border-yellow-400 p-4 my-4\">\n          <p className=\"font-semibold\">⭐ 4.9 out of 5 stars (487 ratings)</p>\n        </div>\n\n        <p>\n          Tiramisu, which means \"pick me up\" in Italian, is one of Italy's most\n          beloved desserts. This authentic recipe creates the perfect balance of\n          coffee-soaked ladyfingers, creamy mascarpone, and rich cocoa.\n        </p>\n\n        <div className=\"grid grid-cols-3 gap-4 my-8 text-center\">\n          <div className=\"bg-gray-100 p-4 rounded\">\n            <p className=\"font-bold\">Prep Time</p>\n            <p>30 minutes</p>\n          </div>\n          <div className=\"bg-gray-100 p-4 rounded\">\n            <p className=\"font-bold\">Chill Time</p>\n            <p>4 hours</p>\n          </div>\n          <div className=\"bg-gray-100 p-4 rounded\">\n            <p className=\"font-bold\">Servings</p>\n            <p>8</p>\n          </div>\n        </div>\n\n        <h2>Ingredients</h2>\n        <ul>\n          <li>6 egg yolks</li>\n          <li>3/4 cup sugar</li>\n          <li>1 1/3 cups mascarpone cheese (room temperature)</li>\n          <li>1 3/4 cups heavy cream</li>\n          <li>2 cups strong espresso (cooled)</li>\n          <li>1/2 cup coffee liqueur</li>\n          <li>2 packages (7 oz each) ladyfinger cookies</li>\n          <li>Unsweetened cocoa powder for dusting</li>\n          <li>Dark chocolate shavings for garnish</li>\n        </ul>\n\n        <h2>Instructions</h2>\n\n        <h3>Prepare the Mascarpone Cream</h3>\n        <ol>\n          <li>\n            Whisk egg yolks and sugar in a double boiler over simmering water\n            until thick and pale\n          </li>\n          <li>Remove from heat and whisk in mascarpone until smooth</li>\n          <li>In a separate bowl, whip cream to stiff peaks</li>\n          <li>Gently fold the whipped cream into mascarpone mixture</li>\n        </ol>\n\n        <h3>Assemble the Tiramisu</h3>\n        <ol>\n          <li>Combine espresso and coffee liqueur in a shallow dish</li>\n          <li>\n            Quickly dip each ladyfinger into coffee mixture and arrange in dish\n          </li>\n          <li>Spread half of mascarpone mixture over ladyfingers</li>\n          <li>Repeat layers with remaining ingredients</li>\n          <li>Cover and refrigerate for at least 4 hours</li>\n          <li>Before serving, dust with cocoa powder and garnish</li>\n        </ol>\n\n        <div className=\"bg-blue-50 p-6 rounded-lg my-8\">\n          <h3>Nutrition Information</h3>\n          <p className=\"text-sm text-gray-600 mb-4\">\n            Per serving (1/8 of dish)\n          </p>\n          <div className=\"grid grid-cols-2 gap-2 text-sm\">\n            <div>Calories: 385</div>\n            <div>Protein: 7g</div>\n            <div>Carbohydrates: 28g</div>\n            <div>Fat: 28g</div>\n            <div>Saturated Fat: 16g</div>\n            <div>Cholesterol: 215mg</div>\n            <div>Sodium: 95mg</div>\n            <div>Sugar: 20g</div>\n          </div>\n        </div>\n\n        <div className=\"bg-gray-100 p-4 rounded-lg my-8\">\n          <h3>Chef's Tips</h3>\n          <ul>\n            <li>Use room temperature mascarpone for the smoothest cream</li>\n            <li>Don't over-soak the ladyfingers - a quick dip is enough</li>\n            <li>For best flavor, make this dessert a day ahead</li>\n            <li>Use high-quality espresso for authentic taste</li>\n          </ul>\n        </div>\n\n        <div className=\"text-center my-8\">\n          <p className=\"text-gray-600 mb-4\">Watch our video tutorial:</p>\n          <div className=\"bg-gray-200 p-8 rounded-lg\">\n            <p>🎥 Video: How to Make Authentic Tiramisu</p>\n            <p className=\"text-sm text-gray-600\">Duration: 12:45</p>\n          </div>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/restaurant/page.tsx",
    "content": "import { LocalBusinessJsonLd } from \"next-seo\";\n\nexport default function RestaurantPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <LocalBusinessJsonLd\n        type=\"Restaurant\"\n        name=\"The Golden Fork\"\n        address={{\n          \"@type\": \"PostalAddress\",\n          streetAddress: \"456 Culinary Avenue\",\n          addressLocality: \"New York\",\n          addressRegion: \"NY\",\n          postalCode: \"10001\",\n          addressCountry: \"US\",\n        }}\n        geo={{\n          \"@type\": \"GeoCoordinates\",\n          latitude: 40.7489,\n          longitude: -73.968,\n        }}\n        url=\"https://example.com/restaurants/golden-fork\"\n        telephone=\"+12125555678\"\n        email=\"info@goldenfork.com\"\n        image={[\n          \"https://example.com/images/restaurant-1x1.jpg\",\n          \"https://example.com/images/restaurant-4x3.jpg\",\n          \"https://example.com/images/restaurant-16x9.jpg\",\n        ]}\n        servesCuisine={[\"Italian\", \"Mediterranean\", \"Vegetarian\"]}\n        priceRange=\"$$$\"\n        menu=\"https://example.com/restaurants/golden-fork/menu\"\n        openingHoursSpecification={[\n          {\n            \"@type\": \"OpeningHoursSpecification\",\n            dayOfWeek: [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\"],\n            opens: \"11:30\",\n            closes: \"22:00\",\n          },\n          {\n            \"@type\": \"OpeningHoursSpecification\",\n            dayOfWeek: [\"Friday\", \"Saturday\"],\n            opens: \"11:30\",\n            closes: \"23:30\",\n          },\n          {\n            \"@type\": \"OpeningHoursSpecification\",\n            dayOfWeek: \"Sunday\",\n            opens: \"12:00\",\n            closes: \"21:00\",\n          },\n        ]}\n        aggregateRating={{\n          \"@type\": \"AggregateRating\",\n          ratingValue: 4.6,\n          ratingCount: 892,\n          reviewCount: 846,\n          bestRating: 5,\n          worstRating: 1,\n        }}\n        review={[\n          {\n            \"@type\": \"Review\",\n            reviewRating: {\n              \"@type\": \"Rating\",\n              ratingValue: 5,\n              bestRating: 5,\n            },\n            author: \"Sarah Johnson\",\n            reviewBody:\n              \"Amazing Italian cuisine! The pasta is made fresh daily and the atmosphere is wonderful.\",\n            datePublished: \"2024-01-15\",\n          },\n          {\n            \"@type\": \"Review\",\n            reviewRating: {\n              \"@type\": \"Rating\",\n              ratingValue: 4,\n              bestRating: 5,\n            },\n            author: {\n              \"@type\": \"Person\",\n              name: \"Mike Chen\",\n            },\n            reviewBody:\n              \"Great food and service. The vegetarian options are excellent.\",\n            datePublished: \"2024-01-20\",\n          },\n        ]}\n        sameAs={[\n          \"https://www.facebook.com/goldenforknyc\",\n          \"https://www.instagram.com/goldenforknyc\",\n          \"https://www.yelp.com/biz/golden-fork-new-york\",\n        ]}\n        slogan=\"Where tradition meets innovation\"\n        description=\"The Golden Fork offers authentic Italian and Mediterranean cuisine in the heart of New York City. Our menu features fresh, locally-sourced ingredients and traditional recipes passed down through generations.\"\n        paymentAccepted=\"Cash, Credit Card, Debit Card\"\n        currenciesAccepted=\"USD\"\n        publicAccess={true}\n        smokingAllowed={false}\n        isAccessibleForFree={true}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>The Golden Fork</h1>\n        <p className=\"text-xl italic\">Where tradition meets innovation</p>\n\n        <p>\n          Welcome to The Golden Fork, New York's premier destination for\n          authentic Italian and Mediterranean cuisine. Located in the heart of\n          Manhattan, we offer a dining experience that combines traditional\n          recipes with modern culinary techniques.\n        </p>\n\n        <h2>Our Cuisine</h2>\n        <p>\n          We specialize in Italian, Mediterranean, and Vegetarian dishes,\n          prepared with fresh, locally-sourced ingredients. Our menu changes\n          seasonally to ensure the best flavors throughout the year.\n        </p>\n\n        <h2>Hours of Operation</h2>\n        <ul>\n          <li>Monday - Thursday: 11:30 AM - 10:00 PM</li>\n          <li>Friday - Saturday: 11:30 AM - 11:30 PM</li>\n          <li>Sunday: 12:00 PM - 9:00 PM</li>\n        </ul>\n\n        <h2>Location</h2>\n        <address>\n          456 Culinary Avenue\n          <br />\n          New York, NY 10001\n          <br />\n          Phone: (212) 555-5678\n          <br />\n          Email: info@goldenfork.com\n        </address>\n\n        <h2>Customer Reviews</h2>\n        <p>\n          With an average rating of 4.6 out of 5 stars from over 890 reviews,\n          The Golden Fork is one of New York's most beloved restaurants.\n        </p>\n\n        <h2>Reservations</h2>\n        <p>\n          Call us at (212) 555-5678 or book online through our website. We\n          recommend making reservations for Friday and Saturday evenings.\n        </p>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/review/page.tsx",
    "content": "import { ReviewJsonLd } from \"next-seo\";\n\nexport default function Page() {\n  return (\n    <div className=\"container mx-auto p-8 space-y-6\">\n      <h1 className=\"text-2xl font-semibold\">ReviewJsonLd Example</h1>\n      <ReviewJsonLd\n        author=\"Bob Smith\"\n        reviewRating={{ ratingValue: 4 }}\n        itemReviewed={{ \"@type\": \"LocalBusiness\", name: \"Legal Seafood\" }}\n        reviewBody=\"Fresh seafood and great service!\"\n        datePublished=\"2024-01-01\"\n        url=\"/review\"\n      />\n      <p>\n        This page demonstrates a standalone Review JSON-LD for a local business.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/review-advanced/page.tsx",
    "content": "import { ProductJsonLd } from \"next-seo\";\n\nexport default function Page() {\n  return (\n    <div className=\"container mx-auto p-8 space-y-6\">\n      <h1 className=\"text-2xl font-semibold\">Product with Nested Reviews</h1>\n      <ProductJsonLd\n        name=\"The Catcher in the Rye\"\n        brand=\"Penguin Books\"\n        description=\"The Catcher in the Rye is a classic coming-of-age story: an story of teenage alienation, capturing the human need for connection and the bewildering sense of loss as we leave childhood behind.\"\n        sku=\"9780241984758\"\n        mpn=\"925872\"\n        image=\"https://example.com/catcher-in-the-rye-book-cover.jpg\"\n        review={[\n          {\n            reviewRating: {\n              ratingValue: 5,\n            },\n            author: \"John Doe\",\n            reviewBody:\n              \"A timeless classic that captures the essence of teenage angst perfectly.\",\n            datePublished: \"2024-01-01\",\n          },\n          {\n            reviewRating: {\n              ratingValue: 4,\n            },\n            author: {\n              name: \"Jane Smith\",\n              url: \"https://example.com/reviewers/jane\",\n            },\n            reviewBody: \"Compelling narrative, though some parts feel dated.\",\n            datePublished: \"2024-01-15\",\n          },\n          {\n            reviewRating: {\n              ratingValue: 3,\n            },\n            author: \"Literary Review Magazine\",\n            reviewBody:\n              \"While historically significant, modern readers may find it less relatable.\",\n            datePublished: \"2024-02-01\",\n          },\n        ]}\n        aggregateRating={{\n          ratingValue: 4.2,\n          bestRating: 5,\n          ratingCount: 150,\n          reviewCount: 120,\n        }}\n        offers={{\n          price: 12.99,\n          priceCurrency: \"USD\",\n          priceValidUntil: \"2024-12-31\",\n          itemCondition: \"NewCondition\",\n          availability: \"InStock\",\n          seller: {\n            name: \"Book Emporium\",\n          },\n        }}\n      />\n      <div className=\"prose max-w-none\">\n        <h2>Product Details</h2>\n        <p>\n          This example demonstrates how reviews can be nested within a Product\n          using ProductJsonLd. The product includes multiple reviews from\n          different authors and an aggregate rating summarizing all reviews.\n        </p>\n        <h3>Features Demonstrated:</h3>\n        <ul>\n          <li>Multiple nested reviews within a product</li>\n          <li>\n            Different author formats (string, object with URL, organization)\n          </li>\n          <li>Aggregate rating alongside individual reviews</li>\n          <li>Complete product information with offers</li>\n        </ul>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/review-movie/page.tsx",
    "content": "import { ReviewJsonLd } from \"next-seo\";\n\nexport default function Page() {\n  return (\n    <div className=\"container mx-auto p-8 space-y-6\">\n      <h1 className=\"text-2xl font-semibold\">Movie Review Example</h1>\n      <ReviewJsonLd\n        author={{\n          name: \"Roger Ebert\",\n          url: \"https://example.com/reviewers/roger-ebert\",\n        }}\n        reviewRating={{\n          ratingValue: 4,\n          bestRating: 4,\n          worstRating: 0,\n        }}\n        itemReviewed={{\n          \"@type\": \"Movie\",\n          name: \"The Shawshank Redemption\",\n          director: \"Frank Darabont\",\n          actor: [\"Tim Robbins\", \"Morgan Freeman\"],\n          dateCreated: \"1994-09-23\",\n          image: \"https://example.com/shawshank.jpg\",\n          duration: \"PT2H22M\",\n        }}\n        reviewBody=\"A masterful adaptation of Stephen King's novella, The Shawshank Redemption is a powerful tale of hope and friendship that resonates deeply with audiences. The performances by Robbins and Freeman are nothing short of extraordinary.\"\n        datePublished=\"2024-03-15\"\n        publisher={{\n          name: \"Film Critics United\",\n          logo: \"https://example.com/fcu-logo.jpg\",\n        }}\n        url=\"https://example.com/reviews/shawshank-redemption\"\n        mainEntityOfPage=\"https://example.com/reviews/shawshank-redemption\"\n      />\n      <div className=\"prose max-w-none\">\n        <h2>About This Review</h2>\n        <p>\n          This example shows a movie review with comprehensive structured data\n          including the movie details, reviewer information, and publisher\n          details.\n        </p>\n        <h3>Key Features:</h3>\n        <ul>\n          <li>Movie-specific properties (director, actors, duration)</li>\n          <li>Custom rating scale (0-4 stars)</li>\n          <li>Publisher with logo</li>\n          <li>Main entity of page reference</li>\n        </ul>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/social-media-posting/page.tsx",
    "content": "import { DiscussionForumPostingJsonLd } from \"next-seo\";\n\nexport default function SocialMediaPostingPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <DiscussionForumPostingJsonLd\n        type=\"SocialMediaPosting\"\n        author={{\n          name: \"TechInfluencer\",\n          url: \"https://example.com/user/techinfluencer\",\n        }}\n        datePublished=\"2024-01-15T14:30:00+00:00\"\n        text=\"Just discovered this amazing new AI tool that's revolutionizing content creation! Check it out 🚀\"\n        sharedContent={{\n          url: \"https://example.com/ai-tool-review\",\n          name: \"Revolutionary AI Tool for Content Creators\",\n          description:\n            \"A comprehensive review of the latest AI tool that's changing how we create content\",\n        }}\n        interactionStatistic={[\n          {\n            interactionType: \"https://schema.org/LikeAction\",\n            userInteractionCount: 342,\n          },\n          {\n            interactionType: \"https://schema.org/ShareAction\",\n            userInteractionCount: 89,\n          },\n          {\n            interactionType: \"https://schema.org/ViewAction\",\n            userInteractionCount: 5678,\n          },\n        ]}\n        comment={[\n          {\n            text: \"Thanks for sharing! I've been looking for something like this.\",\n            author: \"ContentCreator123\",\n            datePublished: \"2024-01-15T15:00:00+00:00\",\n          },\n          {\n            text: \"How does this compare to other AI tools on the market?\",\n            author: {\n              name: \"CuriousDev\",\n              url: \"https://example.com/user/curiousdev\",\n            },\n            datePublished: \"2024-01-15T15:30:00+00:00\",\n          },\n        ]}\n      />\n\n      <article className=\"max-w-2xl mx-auto\">\n        <div className=\"bg-white rounded-lg shadow-md p-6\">\n          <div className=\"flex items-center mb-4\">\n            <div className=\"w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center text-white font-bold\">\n              TI\n            </div>\n            <div className=\"ml-3\">\n              <a\n                href=\"https://example.com/user/techinfluencer\"\n                className=\"font-semibold text-lg\"\n              >\n                TechInfluencer\n              </a>\n              <p className=\"text-sm text-gray-500\">\n                January 15, 2024 at 2:30 PM\n              </p>\n            </div>\n          </div>\n\n          <p className=\"mb-4\">\n            Just discovered this amazing new AI tool that's revolutionizing\n            content creation! Check it out 🚀\n          </p>\n\n          <div className=\"bg-gray-100 p-4 rounded-lg mb-4\">\n            <h3 className=\"font-semibold\">\n              <a\n                href=\"https://example.com/ai-tool-review\"\n                className=\"text-blue-600 hover:underline\"\n              >\n                Revolutionary AI Tool for Content Creators\n              </a>\n            </h3>\n            <p className=\"text-sm text-gray-600 mt-1\">\n              A comprehensive review of the latest AI tool that's changing how\n              we create content\n            </p>\n          </div>\n\n          <div className=\"flex items-center justify-between text-gray-600 border-t pt-3\">\n            <span>👍 342</span>\n            <span>🔄 89 shares</span>\n            <span>👁️ 5,678 views</span>\n          </div>\n\n          <div className=\"mt-6 space-y-4 border-t pt-4\">\n            <h3 className=\"font-semibold\">Comments</h3>\n\n            <div className=\"bg-gray-50 p-3 rounded\">\n              <p className=\"font-semibold text-sm\">ContentCreator123</p>\n              <p className=\"text-sm\">\n                Thanks for sharing! I've been looking for something like this.\n              </p>\n              <p className=\"text-xs text-gray-500 mt-1\">\n                January 15, 2024 at 3:00 PM\n              </p>\n            </div>\n\n            <div className=\"bg-gray-50 p-3 rounded\">\n              <p className=\"font-semibold text-sm\">\n                <a\n                  href=\"https://example.com/user/curiousdev\"\n                  className=\"text-blue-600\"\n                >\n                  CuriousDev\n                </a>\n              </p>\n              <p className=\"text-sm\">\n                How does this compare to other AI tools on the market?\n              </p>\n              <p className=\"text-xs text-gray-500 mt-1\">\n                January 15, 2024 at 3:30 PM\n              </p>\n            </div>\n          </div>\n        </div>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/software-app/page.tsx",
    "content": "import { SoftwareApplicationJsonLd } from \"next-seo\";\n\nexport default function SoftwareAppPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <SoftwareApplicationJsonLd\n        name=\"Task Master Pro\"\n        description=\"A powerful task management app to boost your productivity\"\n        applicationCategory=\"ProductivityApplication\"\n        operatingSystem=\"Windows 10+, macOS 10.15+, Ubuntu 20.04+\"\n        offers={{\n          price: 0,\n          priceCurrency: \"USD\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.5,\n          ratingCount: 1250,\n          reviewCount: 980,\n        }}\n        screenshot={[\n          \"https://example.com/screenshots/dashboard.jpg\",\n          \"https://example.com/screenshots/task-list.jpg\",\n          \"https://example.com/screenshots/calendar-view.jpg\",\n        ]}\n        featureList={[\n          \"Intuitive task organization\",\n          \"Calendar integration\",\n          \"Team collaboration\",\n          \"Progress tracking\",\n          \"Mobile sync\",\n        ]}\n        softwareVersion=\"3.2.1\"\n        datePublished=\"2022-06-15\"\n        dateModified=\"2024-11-28\"\n        author=\"Productivity Labs Inc.\"\n        publisher={{\n          name: \"Productivity Labs Inc.\",\n          url: \"https://productivitylabs.com\",\n        }}\n      />\n\n      <div className=\"max-w-4xl\">\n        <h1 className=\"text-4xl font-bold mb-4\">Task Master Pro</h1>\n        <p className=\"text-xl text-gray-600 mb-6\">\n          A powerful task management app to boost your productivity\n        </p>\n\n        <div className=\"bg-white rounded-lg shadow-lg p-6 mb-8\">\n          <div className=\"flex items-center justify-between mb-4\">\n            <div>\n              <h2 className=\"text-2xl font-semibold\">Free Forever</h2>\n              <p className=\"text-gray-600\">No credit card required</p>\n            </div>\n            <button className=\"bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition\">\n              Download Now\n            </button>\n          </div>\n\n          <div className=\"flex items-center space-x-4\">\n            <div className=\"flex items-center\">\n              <span className=\"text-yellow-400 text-xl\">★★★★☆</span>\n              <span className=\"ml-2 text-gray-600\">4.5/5 (1,250 ratings)</span>\n            </div>\n          </div>\n        </div>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Key Features</h2>\n          <ul className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n            <li className=\"flex items-start\">\n              <span className=\"text-green-500 mr-2\">✓</span>\n              <span>Intuitive task organization with drag-and-drop</span>\n            </li>\n            <li className=\"flex items-start\">\n              <span className=\"text-green-500 mr-2\">✓</span>\n              <span>Seamless calendar integration</span>\n            </li>\n            <li className=\"flex items-start\">\n              <span className=\"text-green-500 mr-2\">✓</span>\n              <span>Real-time team collaboration</span>\n            </li>\n            <li className=\"flex items-start\">\n              <span className=\"text-green-500 mr-2\">✓</span>\n              <span>Advanced progress tracking and analytics</span>\n            </li>\n            <li className=\"flex items-start\">\n              <span className=\"text-green-500 mr-2\">✓</span>\n              <span>Sync across all your devices</span>\n            </li>\n          </ul>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">System Requirements</h2>\n          <div className=\"bg-gray-50 p-4 rounded-lg\">\n            <p className=\"font-medium mb-2\">Supported Operating Systems:</p>\n            <ul className=\"list-disc list-inside text-gray-700\">\n              <li>Windows 10 or later</li>\n              <li>macOS 10.15 (Catalina) or later</li>\n              <li>Ubuntu 20.04 LTS or later</li>\n            </ul>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Screenshots</h2>\n          <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n            <div className=\"bg-gray-200 h-48 rounded-lg flex items-center justify-center\">\n              <span className=\"text-gray-500\">Dashboard View</span>\n            </div>\n            <div className=\"bg-gray-200 h-48 rounded-lg flex items-center justify-center\">\n              <span className=\"text-gray-500\">Task List</span>\n            </div>\n            <div className=\"bg-gray-200 h-48 rounded-lg flex items-center justify-center\">\n              <span className=\"text-gray-500\">Calendar View</span>\n            </div>\n          </div>\n        </section>\n\n        <section>\n          <h2 className=\"text-2xl font-semibold mb-4\">About the Developer</h2>\n          <p className=\"text-gray-700\">\n            Productivity Labs Inc. is dedicated to creating tools that help\n            individuals and teams work more efficiently. With over 10 years of\n            experience in productivity software, we understand what it takes to\n            build tools that people love to use every day.\n          </p>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/software-app-paid/page.tsx",
    "content": "import { SoftwareApplicationJsonLd } from \"next-seo\";\n\nexport default function SoftwareAppPaidPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <SoftwareApplicationJsonLd\n        type=\"DesignApplication\"\n        name=\"Studio Pro - Advanced Photo Editor\"\n        description=\"Professional-grade photo editing software with AI-powered tools\"\n        url=\"https://example.com/studio-pro\"\n        image={{\n          url: \"https://example.com/studio-pro-icon.png\",\n          width: 512,\n          height: 512,\n        }}\n        applicationCategory=\"DesignApplication\"\n        applicationSubCategory=\"PhotoEditing\"\n        applicationSuite=\"Creative Studio Suite\"\n        operatingSystem=\"Windows 11, macOS 12.0+\"\n        memoryRequirements=\"8GB RAM minimum, 16GB recommended\"\n        processorRequirements=\"Intel Core i5 or AMD Ryzen 5 or better\"\n        storageRequirements=\"4GB available space\"\n        offers={{\n          price: 79.99,\n          priceCurrency: \"USD\",\n          availability: \"https://schema.org/InStock\",\n          validFrom: \"2024-01-01\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.8,\n          ratingCount: 3450,\n          reviewCount: 2890,\n          bestRating: 5,\n          worstRating: 1,\n        }}\n        review={[\n          {\n            author: \"Sarah Johnson\",\n            reviewRating: {\n              ratingValue: 5,\n              bestRating: 5,\n            },\n            reviewBody:\n              \"Best photo editing software I've ever used. The AI features save me hours of work!\",\n            datePublished: \"2024-10-15\",\n          },\n          {\n            author: {\n              name: \"Mike Chen\",\n              url: \"https://example.com/users/mikechen\",\n            },\n            reviewRating: {\n              ratingValue: 4,\n            },\n            reviewBody:\n              \"Great features, but takes some time to learn. Worth the investment for professionals.\",\n            datePublished: \"2024-09-22\",\n          },\n        ]}\n        screenshot={[\n          {\n            url: \"https://example.com/screenshots/studio-pro-main.jpg\",\n            caption: \"Main editing interface\",\n          },\n          {\n            url: \"https://example.com/screenshots/studio-pro-ai-tools.jpg\",\n            caption: \"AI-powered enhancement tools\",\n          },\n          {\n            url: \"https://example.com/screenshots/studio-pro-filters.jpg\",\n            caption: \"Professional filter library\",\n          },\n        ]}\n        featureList={[\n          \"AI-powered background removal\",\n          \"Advanced color grading tools\",\n          \"Non-destructive editing\",\n          \"RAW file processing\",\n          \"Batch processing\",\n          \"Cloud sync and backup\",\n          \"Plugin ecosystem\",\n          \"4K and 8K support\",\n        ]}\n        softwareVersion=\"2024.2.3\"\n        releaseNotes=\"New AI upscaling feature, improved performance, bug fixes\"\n        datePublished=\"2021-03-15\"\n        dateModified=\"2024-11-15\"\n        author={{\n          name: \"Creative Software Labs\",\n          url: \"https://creativesoftwarelabs.com\",\n        }}\n        publisher={{\n          name: \"Creative Software Labs\",\n          url: \"https://creativesoftwarelabs.com\",\n          logo: {\n            url: \"https://creativesoftwarelabs.com/logo.png\",\n            width: 600,\n            height: 60,\n          },\n        }}\n        downloadUrl=\"https://example.com/downloads/studio-pro-installer\"\n        countriesSupported={[\"US\", \"CA\", \"GB\", \"AU\", \"NZ\", \"IE\"]}\n      />\n\n      <div className=\"max-w-4xl\">\n        <div className=\"flex items-start justify-between mb-8\">\n          <div>\n            <h1 className=\"text-4xl font-bold mb-2\">Studio Pro</h1>\n            <p className=\"text-xl text-gray-600\">Advanced Photo Editor</p>\n          </div>\n          <div className=\"text-right\">\n            <div className=\"text-3xl font-bold text-green-600\">$79.99</div>\n            <div className=\"text-sm text-gray-500\">One-time purchase</div>\n          </div>\n        </div>\n\n        <div className=\"bg-gradient-to-r from-purple-600 to-blue-600 text-white rounded-lg p-8 mb-8\">\n          <h2 className=\"text-2xl font-bold mb-4\">\n            Professional Photo Editing Made Simple\n          </h2>\n          <p className=\"mb-6\">\n            Transform your photos with AI-powered tools and professional-grade\n            features. Join over 100,000 creative professionals who trust Studio\n            Pro.\n          </p>\n          <div className=\"flex items-center space-x-4\">\n            <button className=\"bg-white text-gray-900 px-6 py-3 rounded-lg font-semibold hover:bg-gray-100 transition\">\n              Buy Now - $79.99\n            </button>\n            <button className=\"border-2 border-white text-white px-6 py-3 rounded-lg font-semibold hover:bg-white hover:text-gray-900 transition\">\n              Free Trial (30 days)\n            </button>\n          </div>\n        </div>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6 mb-8\">\n          <div className=\"bg-white rounded-lg shadow-lg p-6\">\n            <div className=\"text-yellow-400 text-2xl mb-2\">★★★★★</div>\n            <div className=\"text-3xl font-bold mb-1\">4.8/5</div>\n            <div className=\"text-gray-600\">3,450 ratings</div>\n          </div>\n          <div className=\"bg-white rounded-lg shadow-lg p-6\">\n            <div className=\"text-blue-600 text-2xl mb-2\">💾</div>\n            <div className=\"text-3xl font-bold mb-1\">4GB</div>\n            <div className=\"text-gray-600\">Storage required</div>\n          </div>\n          <div className=\"bg-white rounded-lg shadow-lg p-6\">\n            <div className=\"text-green-600 text-2xl mb-2\">✓</div>\n            <div className=\"text-3xl font-bold mb-1\">30-day</div>\n            <div className=\"text-gray-600\">Free trial</div>\n          </div>\n        </div>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Key Features</h2>\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n            <div className=\"bg-gray-50 p-6 rounded-lg\">\n              <h3 className=\"font-semibold mb-2\">AI-Powered Tools</h3>\n              <p className=\"text-gray-700\">\n                Automatic background removal, object selection, and image\n                enhancement powered by advanced machine learning.\n              </p>\n            </div>\n            <div className=\"bg-gray-50 p-6 rounded-lg\">\n              <h3 className=\"font-semibold mb-2\">Professional Workflows</h3>\n              <p className=\"text-gray-700\">\n                Non-destructive editing, layer management, and batch processing\n                for efficient professional workflows.\n              </p>\n            </div>\n            <div className=\"bg-gray-50 p-6 rounded-lg\">\n              <h3 className=\"font-semibold mb-2\">RAW Processing</h3>\n              <p className=\"text-gray-700\">\n                Full support for RAW files from all major camera manufacturers\n                with advanced color grading tools.\n              </p>\n            </div>\n            <div className=\"bg-gray-50 p-6 rounded-lg\">\n              <h3 className=\"font-semibold mb-2\">Cloud Integration</h3>\n              <p className=\"text-gray-700\">\n                Seamless cloud sync and backup, work on your projects from any\n                device with Studio Pro installed.\n              </p>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">System Requirements</h2>\n          <div className=\"bg-gray-50 p-6 rounded-lg\">\n            <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n              <div>\n                <h3 className=\"font-semibold mb-2\">Minimum Requirements</h3>\n                <ul className=\"list-disc list-inside text-gray-700 space-y-1\">\n                  <li>Windows 11 or macOS 12.0+</li>\n                  <li>Intel Core i5 or AMD Ryzen 5</li>\n                  <li>8GB RAM</li>\n                  <li>4GB available storage</li>\n                  <li>OpenGL 3.3 compatible graphics</li>\n                </ul>\n              </div>\n              <div>\n                <h3 className=\"font-semibold mb-2\">Recommended</h3>\n                <ul className=\"list-disc list-inside text-gray-700 space-y-1\">\n                  <li>Latest OS version</li>\n                  <li>Intel Core i7 or AMD Ryzen 7</li>\n                  <li>16GB RAM or more</li>\n                  <li>SSD with 10GB+ available</li>\n                  <li>Dedicated GPU with 4GB VRAM</li>\n                </ul>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-8\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Customer Reviews</h2>\n          <div className=\"space-y-4\">\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <div className=\"flex items-center mb-2\">\n                <span className=\"text-yellow-400\">★★★★★</span>\n                <span className=\"ml-2 font-semibold\">Sarah Johnson</span>\n                <span className=\"ml-auto text-gray-500 text-sm\">\n                  October 15, 2024\n                </span>\n              </div>\n              <p className=\"text-gray-700\">\n                Best photo editing software I've ever used. The AI features save\n                me hours of work!\n              </p>\n            </div>\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <div className=\"flex items-center mb-2\">\n                <span className=\"text-yellow-400\">★★★★☆</span>\n                <span className=\"ml-2 font-semibold\">Mike Chen</span>\n                <span className=\"ml-auto text-gray-500 text-sm\">\n                  September 22, 2024\n                </span>\n              </div>\n              <p className=\"text-gray-700\">\n                Great features, but takes some time to learn. Worth the\n                investment for professionals.\n              </p>\n            </div>\n          </div>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/store-with-departments/page.tsx",
    "content": "import { LocalBusinessJsonLd } from \"next-seo\";\n\nexport default function StoreWithDepartmentsPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <LocalBusinessJsonLd\n        type=\"Store\"\n        name=\"MegaMart Superstore\"\n        address={{\n          \"@type\": \"PostalAddress\",\n          streetAddress: \"789 Shopping Plaza Drive\",\n          addressLocality: \"Los Angeles\",\n          addressRegion: \"CA\",\n          postalCode: \"90028\",\n          addressCountry: \"US\",\n        }}\n        geo={{\n          \"@type\": \"GeoCoordinates\",\n          latitude: 34.1022,\n          longitude: -118.3281,\n        }}\n        url=\"https://example.com/stores/megamart-la\"\n        telephone=\"+13235554321\"\n        image=\"https://example.com/images/megamart-storefront.jpg\"\n        priceRange=\"$$\"\n        openingHoursSpecification={{\n          \"@type\": \"OpeningHoursSpecification\",\n          dayOfWeek: [\n            \"Monday\",\n            \"Tuesday\",\n            \"Wednesday\",\n            \"Thursday\",\n            \"Friday\",\n            \"Saturday\",\n            \"Sunday\",\n          ],\n          opens: \"08:00\",\n          closes: \"22:00\",\n        }}\n        department={[\n          {\n            type: \"Pharmacy\",\n            name: \"MegaMart Pharmacy\",\n            address: \"789 Shopping Plaza Drive, Los Angeles, CA 90028\",\n            telephone: \"+13235554322\",\n            priceRange: \"$\",\n            openingHoursSpecification: [\n              {\n                \"@type\": \"OpeningHoursSpecification\",\n                dayOfWeek: [\n                  \"Monday\",\n                  \"Tuesday\",\n                  \"Wednesday\",\n                  \"Thursday\",\n                  \"Friday\",\n                ],\n                opens: \"09:00\",\n                closes: \"20:00\",\n              },\n              {\n                \"@type\": \"OpeningHoursSpecification\",\n                dayOfWeek: \"Saturday\",\n                opens: \"09:00\",\n                closes: \"18:00\",\n              },\n              {\n                \"@type\": \"OpeningHoursSpecification\",\n                dayOfWeek: \"Sunday\",\n                opens: \"10:00\",\n                closes: \"17:00\",\n              },\n            ],\n            description:\n              \"Full-service pharmacy with prescription services and health consultations\",\n          },\n          {\n            type: \"AutoPartsStore\",\n            name: \"MegaMart Auto Center\",\n            address: \"789 Shopping Plaza Drive, Los Angeles, CA 90028\",\n            telephone: \"+13235554323\",\n            priceRange: \"$$\",\n            openingHoursSpecification: {\n              \"@type\": \"OpeningHoursSpecification\",\n              dayOfWeek: [\n                \"Monday\",\n                \"Tuesday\",\n                \"Wednesday\",\n                \"Thursday\",\n                \"Friday\",\n                \"Saturday\",\n              ],\n              opens: \"08:00\",\n              closes: \"20:00\",\n            },\n            description: \"Complete auto parts and accessories department\",\n          },\n          {\n            type: \"Bakery\",\n            name: \"MegaMart Fresh Bakery\",\n            address: \"789 Shopping Plaza Drive, Los Angeles, CA 90028\",\n            telephone: \"+13235554324\",\n            priceRange: \"$\",\n            openingHoursSpecification: {\n              \"@type\": \"OpeningHoursSpecification\",\n              dayOfWeek: [\n                \"Monday\",\n                \"Tuesday\",\n                \"Wednesday\",\n                \"Thursday\",\n                \"Friday\",\n                \"Saturday\",\n                \"Sunday\",\n              ],\n              opens: \"06:00\",\n              closes: \"20:00\",\n            },\n            description: \"Fresh baked goods made daily on-site\",\n          },\n        ]}\n        sameAs={[\n          \"https://www.facebook.com/megamartla\",\n          \"https://twitter.com/megamartla\",\n        ]}\n        description=\"MegaMart Superstore is your one-stop shop for groceries, pharmacy, auto parts, and more. Serving the Los Angeles community since 1985.\"\n        paymentAccepted=\"Cash, Credit Card, Debit Card, Mobile Payment\"\n        currenciesAccepted=\"USD\"\n        areaServed={[\n          \"Los Angeles\",\n          \"Hollywood\",\n          \"West Hollywood\",\n          \"Beverly Hills\",\n        ]}\n        publicAccess={true}\n      />\n\n      <article className=\"prose lg:prose-xl\">\n        <h1>MegaMart Superstore</h1>\n        <p className=\"text-xl\">Your neighborhood one-stop shop since 1985</p>\n\n        <p>\n          MegaMart Superstore offers everything you need under one roof. From\n          fresh groceries to pharmacy services, auto parts to freshly baked\n          goods, we've got you covered.\n        </p>\n\n        <h2>Store Hours</h2>\n        <p>Main Store: Daily 8:00 AM - 10:00 PM</p>\n\n        <h2>Our Departments</h2>\n\n        <h3>Pharmacy</h3>\n        <p>\n          Our full-service pharmacy offers prescription services, vaccinations,\n          and health consultations.\n        </p>\n        <ul>\n          <li>Mon-Fri: 9:00 AM - 8:00 PM</li>\n          <li>Saturday: 9:00 AM - 6:00 PM</li>\n          <li>Sunday: 10:00 AM - 5:00 PM</li>\n          <li>Phone: (323) 555-4322</li>\n        </ul>\n\n        <h3>Auto Center</h3>\n        <p>\n          Find all your automotive needs including parts, accessories, and\n          maintenance supplies.\n        </p>\n        <ul>\n          <li>Mon-Sat: 8:00 AM - 8:00 PM</li>\n          <li>Sunday: Closed</li>\n          <li>Phone: (323) 555-4323</li>\n        </ul>\n\n        <h3>Fresh Bakery</h3>\n        <p>\n          Start your day with fresh bread, pastries, and custom cakes baked\n          daily on-site.\n        </p>\n        <ul>\n          <li>Daily: 6:00 AM - 8:00 PM</li>\n          <li>Phone: (323) 555-4324</li>\n        </ul>\n\n        <h2>Location</h2>\n        <address>\n          789 Shopping Plaza Drive\n          <br />\n          Los Angeles, CA 90028\n          <br />\n          Main Phone: (323) 555-4321\n        </address>\n\n        <h2>Services Available</h2>\n        <ul>\n          <li>Free parking</li>\n          <li>Wheelchair accessible</li>\n          <li>Online ordering with curbside pickup</li>\n          <li>Home delivery available</li>\n          <li>Senior discount on Tuesdays</li>\n        </ul>\n      </article>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/test-arrays/page.tsx",
    "content": "import { ArticleJsonLd } from \"next-seo\";\n\nexport default function TestArraysPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ArticleJsonLd\n        headline=\"Test Article with Mixed Arrays\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        author={[\n          \"John Doe\",\n          {\n            \"@type\": \"Person\",\n            name: \"Jane Smith\",\n            url: \"https://example.com/jane\",\n          },\n          {\n            \"@type\": \"Organization\",\n            name: \"Tech Corp\",\n            logo: \"https://example.com/logo.png\",\n          },\n        ]}\n        image={[\n          \"https://example.com/image1.jpg\",\n          {\n            \"@type\": \"ImageObject\",\n            url: \"https://example.com/image2.jpg\",\n            width: 800,\n            height: 600,\n          },\n        ]}\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <h1>Test Article with Mixed Arrays</h1>\n        <p>This page tests JSON-LD arrays with mixed content types.</p>\n\n        <h2>Authors</h2>\n        <ul>\n          <li>John Doe (string author)</li>\n          <li>\n            Jane Smith (Person object) -{\" \"}\n            <a href=\"https://example.com/jane\">Profile</a>\n          </li>\n          <li>Tech Corp (Organization object)</li>\n        </ul>\n\n        <h2>Images</h2>\n        <ul>\n          <li>Image 1: https://example.com/image1.jpg (string URL)</li>\n          <li>\n            Image 2: https://example.com/image2.jpg (ImageObject with dimensions\n            800x600)\n          </li>\n        </ul>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/test-nested/page.tsx",
    "content": "import { RecipeJsonLd } from \"next-seo\";\n\nexport default function TestNestedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <RecipeJsonLd\n        name=\"Test Recipe\"\n        image=\"https://example.com/images/chocolate-chip-cookies.jpg\"\n        recipeIngredient={[\"1 cup flour\", \"2 eggs\", \"1/2 cup milk\"]}\n        recipeInstructions={[\n          {\n            \"@type\": \"HowToStep\",\n            name: \"Step 1\",\n            text: \"Do something\",\n            image: {\n              \"@type\": \"ImageObject\",\n              url: \"https://example.com/step1.jpg\",\n              width: 300,\n              height: 200,\n            },\n          },\n        ]}\n        nutrition={{\n          \"@type\": \"NutritionInformation\",\n          calories: \"250 calories\",\n          servingSize: \"1 serving\",\n        }}\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <h1>Test Nested JSON Structures</h1>\n        <p>This page tests deeply nested JSON-LD structures for validation.</p>\n\n        <h2>Recipe: Test Recipe</h2>\n        <div className=\"border p-4 rounded bg-gray-50\">\n          <h3>Nutrition Information</h3>\n          <ul>\n            <li>Calories: 250 calories</li>\n            <li>Serving Size: 1 serving</li>\n          </ul>\n\n          <h3>Instructions</h3>\n          <div className=\"mt-4\">\n            <h4>Step 1: Do something</h4>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/test-url-params/page.tsx",
    "content": "import { ArticleJsonLd } from \"next-seo\";\n\nexport default function TestUrlParamsPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        url=\"https://example.com/article?title=yes&page=1&utm_source=google&filter=new\"\n        mainEntityOfPage=\"https://example.com/main?category=tech&sort=date\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        author={{\n          \"@type\": \"Person\",\n          name: \"John Doe\",\n          url: \"https://example.com/authors/john?bio=full&lang=en\",\n        }}\n        publisher={{\n          name: \"Example Corp\",\n          url: \"https://example.com?ref=article&campaign=2024\",\n        }}\n      />\n\n      <div className=\"prose lg:prose-xl\">\n        <h1>Test Article with URL Parameters</h1>\n        <p>\n          This page tests that URL query parameters are preserved correctly in\n          JSON-LD.\n        </p>\n\n        <h2>URLs with Parameters</h2>\n        <ul>\n          <li>\n            <strong>Article URL:</strong>{\" \"}\n            <code>\n              https://example.com/article?title=yes&page=1&utm_source=google&filter=new\n            </code>\n          </li>\n          <li>\n            <strong>Main Entity:</strong>{\" \"}\n            <code>https://example.com/main?category=tech&sort=date</code>\n          </li>\n          <li>\n            <strong>Author URL:</strong>{\" \"}\n            <code>https://example.com/authors/john?bio=full&lang=en</code>\n          </li>\n          <li>\n            <strong>Publisher URL:</strong>{\" \"}\n            <code>https://example.com?ref=article&campaign=2024</code>\n          </li>\n        </ul>\n\n        <p>All query parameters should be preserved exactly as specified.</p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/vacation-rental/page.tsx",
    "content": "import { VacationRentalJsonLd } from \"next-seo\";\n\nexport default function VacationRentalPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <VacationRentalJsonLd\n        containsPlace={{\n          occupancy: {\n            value: 5,\n          },\n        }}\n        identifier=\"beach-house-123\"\n        image=\"https://example.com/vacation-rental-main.jpg\"\n        latitude={42.12345}\n        longitude={-71.98765}\n        name=\"Beautiful Beach House\"\n      />\n\n      <h1 className=\"text-4xl font-bold mb-4\">Beautiful Beach House</h1>\n\n      <div className=\"prose lg:prose-xl\">\n        <h2>About this rental</h2>\n        <p>\n          Welcome to our beautiful beach house! This stunning property can\n          accommodate up to 5 guests and offers breathtaking ocean views.\n          Perfect for families or groups looking for a relaxing getaway.\n        </p>\n\n        <h3>Location</h3>\n        <p>\n          Located at coordinates 42.12345, -71.98765, our beach house is just\n          steps away from the pristine sandy beach. Enjoy morning walks along\n          the shore and spectacular sunsets from your private deck.\n        </p>\n\n        <h3>Occupancy</h3>\n        <p>Maximum occupancy: 5 guests</p>\n\n        <h3>Property ID</h3>\n        <p>Listing ID: beach-house-123</p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/vacation-rental-advanced/page.tsx",
    "content": "import { VacationRentalJsonLd } from \"next-seo\";\n\nexport default function VacationRentalAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <VacationRentalJsonLd\n        containsPlace={{\n          additionalType: \"EntirePlace\",\n          bed: [\n            {\n              numberOfBeds: 1,\n              typeOfBed: \"Queen\",\n            },\n            {\n              numberOfBeds: 2,\n              typeOfBed: \"Single\",\n            },\n          ],\n          occupancy: {\n            value: 5,\n          },\n          amenityFeature: [\n            {\n              name: \"ac\",\n              value: true,\n            },\n            {\n              name: \"wifi\",\n              value: true,\n            },\n            {\n              name: \"beachAccess\",\n              value: true,\n            },\n            {\n              name: \"pool\",\n              value: true,\n            },\n            {\n              name: \"balcony\",\n              value: true,\n            },\n            {\n              name: \"kitchen\",\n              value: true,\n            },\n            {\n              name: \"washerDryer\",\n              value: true,\n            },\n            {\n              name: \"poolType\",\n              value: \"Outdoor\",\n            },\n            {\n              name: \"parkingType\",\n              value: \"Free\",\n            },\n            {\n              name: \"internetType\",\n              value: \"Free\",\n            },\n            {\n              name: \"licenseNum\",\n              value: \"California: VR-12345-2024\",\n            },\n          ],\n          floorSize: {\n            value: 150,\n            unitCode: \"MTK\",\n          },\n          numberOfBathroomsTotal: 2.5,\n          numberOfBedrooms: 3,\n          numberOfRooms: 7,\n          petsAllowed: true,\n          smokingAllowed: false,\n        }}\n        identifier=\"lux-villa-malibu-456\"\n        image={[\n          \"https://example.com/images/villa-exterior.jpg\",\n          \"https://example.com/images/villa-living-room.jpg\",\n          \"https://example.com/images/villa-master-bedroom.jpg\",\n          \"https://example.com/images/villa-guest-bedroom.jpg\",\n          \"https://example.com/images/villa-kitchen.jpg\",\n          \"https://example.com/images/villa-bathroom.jpg\",\n          \"https://example.com/images/villa-pool.jpg\",\n          \"https://example.com/images/villa-ocean-view.jpg\",\n        ]}\n        latitude={34.03654}\n        longitude={-118.68512}\n        name=\"Luxury Ocean View Villa\"\n        additionalType=\"Villa\"\n        address={{\n          addressCountry: \"US\",\n          addressLocality: \"Malibu\",\n          addressRegion: \"California\",\n          postalCode: \"90265\",\n          streetAddress: \"123 Ocean Drive, Unit 6E\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.8,\n          ratingCount: 125,\n          reviewCount: 98,\n          bestRating: 5,\n        }}\n        brand={{\n          name: \"Luxury Beach Rentals Inc\",\n        }}\n        checkinTime=\"15:00:00-08:00\"\n        checkoutTime=\"11:00:00-08:00\"\n        description=\"Stunning beachfront villa with panoramic ocean views, modern amenities, and direct beach access. This luxury property features 3 bedrooms, 2.5 bathrooms, a private pool, and spacious outdoor entertainment areas.\"\n        knowsLanguage={[\"en-US\", \"es-ES\", \"fr-FR\"]}\n        review={[\n          {\n            reviewRating: {\n              ratingValue: 5,\n              bestRating: 5,\n            },\n            author: \"Sarah Johnson\",\n            datePublished: \"2024-01-15\",\n            reviewBody:\n              \"Absolutely stunning property! The ocean views were breathtaking and the villa was even better than the photos. Perfect for our family vacation.\",\n          },\n          {\n            reviewRating: {\n              ratingValue: 4,\n              bestRating: 5,\n            },\n            author: {\n              name: \"Michael Chen\",\n            },\n            datePublished: \"2024-01-20\",\n            reviewBody:\n              \"Great location and beautiful villa. Only minor issue was the hot tub wasn't working for the first day, but the host fixed it quickly.\",\n          },\n          {\n            reviewRating: {\n              ratingValue: 5,\n              bestRating: 5,\n            },\n            author: \"Emma Thompson\",\n            datePublished: \"2024-02-01\",\n            reviewBody:\n              \"We had an amazing stay! The villa is luxurious and well-maintained. The private beach access was a huge plus. Will definitely book again!\",\n          },\n        ]}\n      />\n\n      <h1 className=\"text-4xl font-bold mb-4\">Luxury Ocean View Villa</h1>\n\n      <div className=\"prose lg:prose-xl max-w-none\">\n        <h2>About this luxury villa</h2>\n        <p>\n          {`Stunning beachfront villa with panoramic ocean views, modern amenities,\n          and direct beach access. This luxury property features 3 bedrooms, 2.5\n          bathrooms, a private pool, and spacious outdoor entertainment areas.`}\n        </p>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-2 gap-8 my-8\">\n          <div>\n            <h3>Property Details</h3>\n            <ul>\n              <li>Type: Entire Villa</li>\n              <li>Bedrooms: 3</li>\n              <li>Bathrooms: 2.5</li>\n              <li>Total Rooms: 7</li>\n              <li>Floor Size: 150 m² (1,615 sq ft)</li>\n              <li>Maximum Occupancy: 5 guests</li>\n              <li>License: California: VR-12345-2024</li>\n            </ul>\n          </div>\n\n          <div>\n            <h3>Sleeping Arrangements</h3>\n            <ul>\n              <li>Master Bedroom: 1 Queen bed</li>\n              <li>Guest Bedroom: 2 Single beds</li>\n            </ul>\n\n            <h3>House Rules</h3>\n            <ul>\n              <li>Check-in: 3:00 PM PST</li>\n              <li>Check-out: 11:00 AM PST</li>\n              <li>Pets allowed: Yes</li>\n              <li>Smoking allowed: No</li>\n            </ul>\n          </div>\n        </div>\n\n        <h3>Amenities</h3>\n        <div className=\"grid grid-cols-2 md:grid-cols-3 gap-4\">\n          <div>\n            <h4>Comfort</h4>\n            <ul>\n              <li>✓ Air conditioning</li>\n              <li>✓ Free WiFi</li>\n              <li>✓ Balcony</li>\n            </ul>\n          </div>\n          <div>\n            <h4>Kitchen</h4>\n            <ul>\n              <li>✓ Full kitchen</li>\n              <li>✓ Washer/Dryer</li>\n            </ul>\n          </div>\n          <div>\n            <h4>Entertainment</h4>\n            <ul>\n              <li>✓ Beach access</li>\n              <li>✓ Outdoor pool</li>\n              <li>✓ Free parking</li>\n            </ul>\n          </div>\n        </div>\n\n        <h3>Location</h3>\n        <p>\n          123 Ocean Drive, Unit 6E\n          <br />\n          Malibu, California 90265\n          <br />\n          United States\n        </p>\n        <p>Coordinates: 34.03654°N, 118.68512°W</p>\n\n        <h3>Host Information</h3>\n        <p>\n          Managed by: Luxury Beach Rentals Inc\n          <br />\n          Languages spoken: English, Spanish, French\n        </p>\n\n        <h3>Guest Reviews</h3>\n        <div className=\"bg-gray-100 p-4 rounded-lg mb-4\">\n          <p className=\"font-bold\">Overall Rating: 4.8/5</p>\n          <p className=\"text-sm text-gray-600\">\n            Based on 125 ratings from 98 reviews\n          </p>\n        </div>\n\n        <div className=\"space-y-4\">\n          <div className=\"border-l-4 border-blue-500 pl-4\">\n            <p className=\"font-semibold\">Sarah Johnson - ⭐⭐⭐⭐⭐</p>\n            <p className=\"text-sm text-gray-600\">January 15, 2024</p>\n            <p className=\"mt-2\">\n              Absolutely stunning property! The ocean views were breathtaking\n              and the villa was even better than the photos. Perfect for our\n              family vacation.\n            </p>\n          </div>\n\n          <div className=\"border-l-4 border-blue-500 pl-4\">\n            <p className=\"font-semibold\">Michael Chen - ⭐⭐⭐⭐</p>\n            <p className=\"text-sm text-gray-600\">January 20, 2024</p>\n            <p className=\"mt-2\">\n              Great location and beautiful villa. Only minor issue was the hot\n              tub wasn't working for the first day, but the host fixed it\n              quickly.\n            </p>\n          </div>\n\n          <div className=\"border-l-4 border-blue-500 pl-4\">\n            <p className=\"font-semibold\">Emma Thompson - ⭐⭐⭐⭐⭐</p>\n            <p className=\"text-sm text-gray-600\">February 1, 2024</p>\n            <p className=\"mt-2\">\n              We had an amazing stay! The villa is luxurious and\n              well-maintained. The private beach access was a huge plus. Will\n              definitely book again!\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/vacation-rental-apartment/page.tsx",
    "content": "import { VacationRentalJsonLd } from \"next-seo\";\n\nexport default function VacationRentalApartmentPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <VacationRentalJsonLd\n        containsPlace={{\n          additionalType: \"PrivateRoom\",\n          bed: {\n            numberOfBeds: 1,\n            typeOfBed: \"Double\",\n          },\n          occupancy: {\n            value: 2,\n          },\n          amenityFeature: [\n            {\n              name: \"ac\",\n              value: true,\n            },\n            {\n              name: \"wifi\",\n              value: true,\n            },\n            {\n              name: \"elevator\",\n              value: true,\n            },\n            {\n              name: \"kitchen\",\n              value: false,\n            },\n            {\n              name: \"selfCheckinCheckout\",\n              value: true,\n            },\n            {\n              name: \"parkingType\",\n              value: \"None\",\n            },\n          ],\n          floorSize: {\n            value: 450,\n            unitCode: \"FTK\",\n          },\n          numberOfBathroomsTotal: 1,\n          numberOfBedrooms: 1,\n          numberOfRooms: 2,\n          petsAllowed: false,\n          smokingAllowed: false,\n        }}\n        identifier=\"city-apt-789\"\n        image={[\n          \"https://example.com/apt/bedroom.jpg\",\n          \"https://example.com/apt/bathroom.jpg\",\n          \"https://example.com/apt/entrance.jpg\",\n          \"https://example.com/apt/window-view.jpg\",\n          \"https://example.com/apt/building-exterior.jpg\",\n          \"https://example.com/apt/neighborhood.jpg\",\n          \"https://example.com/apt/amenities.jpg\",\n          \"https://example.com/apt/detail.jpg\",\n        ]}\n        latitude=\"40.74844\"\n        longitude=\"-73.98566\"\n        name=\"Cozy Manhattan Studio Apartment\"\n        additionalType=\"Apartment\"\n        address={{\n          addressCountry: \"US\",\n          addressLocality: \"New York\",\n          addressRegion: \"New York\",\n          postalCode: \"10001\",\n          streetAddress: \"456 Broadway, Apt 12B\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.3,\n          ratingCount: 45,\n          reviewCount: 38,\n        }}\n        checkinTime=\"14:00:00-05:00\"\n        checkoutTime=\"10:00:00-05:00\"\n        description=\"Modern studio apartment in the heart of Manhattan. Perfect for business travelers or couples exploring NYC.\"\n        knowsLanguage=\"en-US\"\n        review={{\n          reviewRating: {\n            ratingValue: 4,\n          },\n          author: {\n            name: \"David Lee\",\n          },\n          datePublished: \"2024-02-10\",\n          reviewBody:\n            \"Great location and clean apartment. Easy self check-in process.\",\n        }}\n      />\n\n      <h1 className=\"text-4xl font-bold mb-4\">\n        Cozy Manhattan Studio Apartment\n      </h1>\n\n      <div className=\"prose lg:prose-xl\">\n        <h2>About this apartment</h2>\n        <p>\n          Modern studio apartment in the heart of Manhattan. Perfect for\n          business travelers or couples exploring NYC. This cozy space offers\n          all the essentials for a comfortable stay in the city.\n        </p>\n\n        <h3>Accommodation Details</h3>\n        <ul>\n          <li>Type: Private Room (Studio)</li>\n          <li>Size: 450 sq ft (42 m²)</li>\n          <li>Floor: 12th with elevator access</li>\n          <li>Maximum occupancy: 2 guests</li>\n          <li>1 bedroom with 1 double bed</li>\n          <li>1 bathroom</li>\n        </ul>\n\n        <h3>Features</h3>\n        <ul>\n          <li>✓ Air conditioning</li>\n          <li>✓ Free WiFi</li>\n          <li>✓ Elevator access</li>\n          <li>✓ Self check-in/checkout</li>\n          <li>✗ No kitchen (kitchenette only)</li>\n          <li>✗ No parking available</li>\n          <li>✗ No pets allowed</li>\n          <li>✗ No smoking</li>\n        </ul>\n\n        <h3>Location</h3>\n        <p>\n          456 Broadway, Apt 12B\n          <br />\n          New York, NY 10001\n          <br />\n          Coordinates: 40.74844°N, 73.98566°W\n        </p>\n\n        <h3>Check-in/Check-out</h3>\n        <ul>\n          <li>Check-in: 2:00 PM EST</li>\n          <li>Check-out: 10:00 AM EST</li>\n          <li>Self check-in available with keypad entry</li>\n        </ul>\n\n        <h3>Guest Reviews</h3>\n        <div className=\"bg-gray-100 p-4 rounded-lg\">\n          <p className=\"font-bold\">Rating: 4.3/5</p>\n          <p className=\"text-sm text-gray-600\">\n            Based on 45 ratings from 38 reviews\n          </p>\n          <blockquote className=\"mt-4 italic\">\n            \"Great location and clean apartment. Easy self check-in process.\"\n            <footer className=\"text-sm mt-2\">— David Lee, February 2024</footer>\n          </blockquote>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/video/page.tsx",
    "content": "import { VideoJsonLd } from \"next-seo\";\n\nexport default function VideoPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <VideoJsonLd\n        name=\"How to Make a Perfect Chocolate Cake\"\n        description=\"Learn how to make the perfect chocolate cake with this easy step-by-step recipe tutorial\"\n        thumbnailUrl=\"https://example.com/chocolate-cake-thumbnail.jpg\"\n        uploadDate=\"2024-01-15T08:00:00+00:00\"\n        contentUrl=\"https://example.com/videos/chocolate-cake-recipe.mp4\"\n        embedUrl=\"https://example.com/embed/chocolate-cake-recipe\"\n        duration=\"PT10M30S\"\n      />\n\n      <main className=\"prose lg:prose-xl mx-auto\">\n        <h1>How to Make a Perfect Chocolate Cake</h1>\n\n        <div className=\"aspect-w-16 aspect-h-9 mb-8 bg-gray-200 rounded-lg p-8 text-center\">\n          <p className=\"text-gray-600\">Video player placeholder</p>\n        </div>\n\n        <p className=\"lead\">\n          Learn how to make the perfect chocolate cake with this easy\n          step-by-step recipe tutorial. This recipe has been perfected over\n          years and guarantees a moist, rich chocolate cake every time.\n        </p>\n\n        <section>\n          <h2>Video Details</h2>\n          <ul>\n            <li>\n              <strong>Duration:</strong> 10 minutes 30 seconds\n            </li>\n            <li>\n              <strong>Uploaded:</strong> January 15, 2024\n            </li>\n            <li>\n              <strong>Format:</strong> MP4 (1080p)\n            </li>\n          </ul>\n        </section>\n\n        <section>\n          <h2>About This Recipe</h2>\n          <p>\n            This chocolate cake recipe has been passed down through generations\n            and refined to perfection. In this video, we'll walk you through\n            every step, from measuring ingredients to the final decoration.\n          </p>\n        </section>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/video-advanced/page.tsx",
    "content": "import { VideoJsonLd } from \"next-seo\";\n\nexport default function VideoAdvancedPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <VideoJsonLd\n        name=\"Complete Baking Masterclass: From Basics to Advanced\"\n        description=\"Join our comprehensive baking masterclass covering everything from basic techniques to advanced pastry skills\"\n        thumbnailUrl={[\n          \"https://example.com/thumbnails/masterclass-1x1.jpg\",\n          \"https://example.com/thumbnails/masterclass-4x3.jpg\",\n          \"https://example.com/thumbnails/masterclass-16x9.jpg\",\n        ]}\n        uploadDate=\"2024-03-01T10:00:00+00:00\"\n        contentUrl=\"https://example.com/videos/baking-masterclass.mp4\"\n        embedUrl=\"https://example.com/embed/baking-masterclass\"\n        duration=\"PT2H30M\"\n        expires=\"2025-03-01T00:00:00+00:00\"\n        interactionStatistic={[\n          {\n            interactionType: \"WatchAction\",\n            userInteractionCount: 500000,\n          },\n          {\n            interactionType: \"LikeAction\",\n            userInteractionCount: 25000,\n          },\n        ]}\n        regionsAllowed={[\"US\", \"CA\", \"GB\", \"AU\", \"NZ\"]}\n        ineligibleRegion={[\"CN\", \"RU\"]}\n        author={[\n          \"Chef Julia Martinez\",\n          {\n            name: \"Chef Paul Anderson\",\n            url: \"https://example.com/chefs/paul-anderson\",\n          },\n        ]}\n        publisher={{\n          name: \"Culinary Institute Online\",\n          logo: \"https://example.com/culinary-institute-logo.png\",\n          url: \"https://example.com\",\n        }}\n      />\n\n      <main className=\"prose lg:prose-xl mx-auto\">\n        <h1>Complete Baking Masterclass: From Basics to Advanced</h1>\n\n        <div className=\"bg-blue-50 p-6 rounded-lg mb-8\">\n          <h3 className=\"text-lg font-semibold mb-2\">Video Statistics</h3>\n          <div className=\"grid grid-cols-2 gap-4\">\n            <div>\n              <p className=\"text-3xl font-bold\">500K+</p>\n              <p className=\"text-gray-600\">Views</p>\n            </div>\n            <div>\n              <p className=\"text-3xl font-bold\">25K+</p>\n              <p className=\"text-gray-600\">Likes</p>\n            </div>\n          </div>\n        </div>\n\n        <section>\n          <h2>About This Masterclass</h2>\n          <p>\n            Join world-renowned chefs Julia Martinez and Paul Anderson in this\n            comprehensive 2.5-hour baking masterclass. Perfect for both\n            beginners and experienced bakers looking to refine their skills.\n          </p>\n        </section>\n\n        <section>\n          <h2>Instructors</h2>\n          <div className=\"grid md:grid-cols-2 gap-6\">\n            <div className=\"bg-gray-50 p-4 rounded-lg\">\n              <h3>Chef Julia Martinez</h3>\n              <p>Award-winning pastry chef with 20 years of experience</p>\n            </div>\n            <div className=\"bg-gray-50 p-4 rounded-lg\">\n              <h3>Chef Paul Anderson</h3>\n              <p>Master baker and cookbook author</p>\n            </div>\n          </div>\n        </section>\n\n        <section>\n          <h2>Availability</h2>\n          <p>\n            This video is available in: United States, Canada, United Kingdom,\n            Australia, and New Zealand. The video will be available until March\n            1, 2025.\n          </p>\n        </section>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/video-clips/page.tsx",
    "content": "import { VideoJsonLd } from \"next-seo\";\n\nexport default function VideoClipsPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <VideoJsonLd\n        name=\"Complete Guide to French Pastries\"\n        description=\"A comprehensive tutorial covering classic French pastries from croissants to éclairs\"\n        thumbnailUrl=\"https://example.com/french-pastries-thumbnail.jpg\"\n        uploadDate=\"2024-02-15T09:00:00+00:00\"\n        contentUrl=\"https://example.com/videos/french-pastries-guide.mp4\"\n        embedUrl=\"https://example.com/embed/french-pastries-guide\"\n        duration=\"PT45M\"\n        interactionStatistic={{\n          interactionType: \"WatchAction\",\n          userInteractionCount: 75000,\n        }}\n        hasPart={[\n          {\n            name: \"Introduction to French Pastries\",\n            startOffset: 0,\n            endOffset: 180,\n            url: \"https://example.com/videos/french-pastries-guide?t=0\",\n          },\n          {\n            name: \"Making Croissants\",\n            startOffset: 180,\n            endOffset: 720,\n            url: \"https://example.com/videos/french-pastries-guide?t=180\",\n          },\n          {\n            name: \"Perfect Pain au Chocolat\",\n            startOffset: 720,\n            endOffset: 1200,\n            url: \"https://example.com/videos/french-pastries-guide?t=720\",\n          },\n          {\n            name: \"Classic Éclairs\",\n            startOffset: 1200,\n            endOffset: 1800,\n            url: \"https://example.com/videos/french-pastries-guide?t=1200\",\n          },\n          {\n            name: \"Fruit Tarts and Final Tips\",\n            startOffset: 1800,\n            endOffset: 2700,\n            url: \"https://example.com/videos/french-pastries-guide?t=1800\",\n          },\n        ]}\n        author=\"Chef Pierre Dubois\"\n        publisher={{\n          name: \"French Culinary Academy\",\n          logo: \"https://example.com/french-culinary-academy-logo.png\",\n        }}\n      />\n\n      <main className=\"prose lg:prose-xl mx-auto\">\n        <h1>Complete Guide to French Pastries</h1>\n\n        <div className=\"aspect-w-16 aspect-h-9 mb-8 bg-gray-200 rounded-lg p-8 text-center\">\n          <p className=\"text-gray-600\">Video player placeholder</p>\n        </div>\n\n        <section className=\"bg-gray-50 p-6 rounded-lg mb-8\">\n          <h2 className=\"text-2xl font-bold mb-4\">Video Chapters</h2>\n          <nav className=\"space-y-3\">\n            <a\n              href=\"#t=0\"\n              className=\"block p-3 bg-white rounded-lg hover:shadow-md transition-shadow\"\n            >\n              <div className=\"flex justify-between items-center\">\n                <div>\n                  <p className=\"font-semibold\">\n                    Introduction to French Pastries\n                  </p>\n                  <p className=\"text-sm text-gray-600\">0:00 - 3:00</p>\n                </div>\n                <span className=\"text-gray-400\">→</span>\n              </div>\n            </a>\n            <a\n              href=\"#t=180\"\n              className=\"block p-3 bg-white rounded-lg hover:shadow-md transition-shadow\"\n            >\n              <div className=\"flex justify-between items-center\">\n                <div>\n                  <p className=\"font-semibold\">Making Croissants</p>\n                  <p className=\"text-sm text-gray-600\">3:00 - 12:00</p>\n                </div>\n                <span className=\"text-gray-400\">→</span>\n              </div>\n            </a>\n            <a\n              href=\"#t=720\"\n              className=\"block p-3 bg-white rounded-lg hover:shadow-md transition-shadow\"\n            >\n              <div className=\"flex justify-between items-center\">\n                <div>\n                  <p className=\"font-semibold\">Perfect Pain au Chocolat</p>\n                  <p className=\"text-sm text-gray-600\">12:00 - 20:00</p>\n                </div>\n                <span className=\"text-gray-400\">→</span>\n              </div>\n            </a>\n            <a\n              href=\"#t=1200\"\n              className=\"block p-3 bg-white rounded-lg hover:shadow-md transition-shadow\"\n            >\n              <div className=\"flex justify-between items-center\">\n                <div>\n                  <p className=\"font-semibold\">Classic Éclairs</p>\n                  <p className=\"text-sm text-gray-600\">20:00 - 30:00</p>\n                </div>\n                <span className=\"text-gray-400\">→</span>\n              </div>\n            </a>\n            <a\n              href=\"#t=1800\"\n              className=\"block p-3 bg-white rounded-lg hover:shadow-md transition-shadow\"\n            >\n              <div className=\"flex justify-between items-center\">\n                <div>\n                  <p className=\"font-semibold\">Fruit Tarts and Final Tips</p>\n                  <p className=\"text-sm text-gray-600\">30:00 - 45:00</p>\n                </div>\n                <span className=\"text-gray-400\">→</span>\n              </div>\n            </a>\n          </nav>\n        </section>\n\n        <section>\n          <h2>About This Tutorial</h2>\n          <p>\n            Master the art of French pastry making with Chef Pierre Dubois in\n            this comprehensive 45-minute tutorial. Each chapter focuses on a\n            different classic French pastry, with detailed step-by-step\n            instructions.\n          </p>\n        </section>\n\n        <section>\n          <h2>What You'll Learn</h2>\n          <ul>\n            <li>The fundamentals of French pastry dough</li>\n            <li>Proper lamination techniques for croissants</li>\n            <li>How to achieve the perfect choux pastry</li>\n            <li>Professional decorating techniques</li>\n            <li>Tips for consistent results every time</li>\n          </ul>\n        </section>\n\n        <div className=\"bg-blue-50 p-6 rounded-lg\">\n          <p className=\"text-lg\">\n            <strong>Pro Tip:</strong> Use the chapter navigation above to jump\n            directly to the pastry you want to learn about. Each section is\n            self-contained with all the information you need.\n          </p>\n        </div>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/video-game/page.tsx",
    "content": "import { SoftwareApplicationJsonLd } from \"next-seo\";\n\nexport default function VideoGamePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <SoftwareApplicationJsonLd\n        type={[\"VideoGame\", \"MobileApplication\"]}\n        name=\"Dragon Quest Legends\"\n        description=\"Epic RPG adventure with stunning graphics, immersive storyline, and multiplayer battles\"\n        url=\"https://example.com/dragon-quest-legends\"\n        image={[\n          {\n            url: \"https://example.com/dragon-quest-icon-1x1.jpg\",\n            width: 1024,\n            height: 1024,\n          },\n          {\n            url: \"https://example.com/dragon-quest-banner-16x9.jpg\",\n            width: 1920,\n            height: 1080,\n          },\n        ]}\n        applicationCategory=\"GameApplication\"\n        applicationSubCategory=\"RolePlaying\"\n        operatingSystem=\"iOS 14.0+, Android 9.0+, Nintendo Switch\"\n        memoryRequirements=\"3GB RAM minimum\"\n        storageRequirements=\"4.5GB\"\n        offers={{\n          price: 19.99,\n          priceCurrency: \"USD\",\n          availability: \"https://schema.org/InStock\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.8,\n          ratingCount: 45000,\n          reviewCount: 35000,\n          bestRating: 5,\n        }}\n        review={[\n          {\n            author: \"GamePro Magazine\",\n            reviewRating: {\n              ratingValue: 5,\n              bestRating: 5,\n            },\n            reviewBody:\n              \"A masterpiece of mobile gaming. Dragon Quest Legends sets a new standard for RPGs on mobile platforms.\",\n            datePublished: \"2024-10-15\",\n          },\n          {\n            author: \"Jessica Chen\",\n            reviewRating: { ratingValue: 4 },\n            reviewBody:\n              \"Amazing graphics and gameplay! The story is captivating, though the game requires a constant internet connection.\",\n            datePublished: \"2024-11-01\",\n          },\n        ]}\n        screenshot={[\n          {\n            url: \"https://example.com/screenshots/dragon-quest-combat.jpg\",\n            caption: \"Epic real-time combat system\",\n          },\n          {\n            url: \"https://example.com/screenshots/dragon-quest-world.jpg\",\n            caption: \"Vast open world to explore\",\n          },\n          {\n            url: \"https://example.com/screenshots/dragon-quest-multiplayer.jpg\",\n            caption: \"Multiplayer raid battles\",\n          },\n          {\n            url: \"https://example.com/screenshots/dragon-quest-character.jpg\",\n            caption: \"Deep character customization\",\n          },\n        ]}\n        featureList={[\n          \"50+ hours of main storyline\",\n          \"Real-time combat system\",\n          \"Multiplayer raids and PvP battles\",\n          \"300+ unique monsters to collect\",\n          \"Character customization with 1000+ items\",\n          \"Weekly events and updates\",\n          \"Cross-platform play\",\n          \"Cloud save synchronization\",\n          \"Controller support\",\n        ]}\n        softwareVersion=\"2.4.1\"\n        releaseNotes=\"New winter event, balance adjustments, bug fixes\"\n        datePublished=\"2023-03-15\"\n        dateModified=\"2024-12-01\"\n        author={{\n          name: \"Legendary Games Studio\",\n          url: \"https://legendarygames.com\",\n        }}\n        publisher={{\n          name: \"Epic Entertainment Corp.\",\n          url: \"https://epicentertainment.com\",\n          logo: {\n            url: \"https://epicentertainment.com/logo.png\",\n            width: 600,\n            height: 60,\n          },\n        }}\n        downloadUrl=\"https://apps.apple.com/app/dragon-quest-legends/id9876543210\"\n        installUrl=\"https://play.google.com/store/apps/details?id=com.epicent.dragonquest\"\n        permissions={[\n          \"Internet access (for multiplayer)\",\n          \"Storage (for game data)\",\n          \"Notifications (for events)\",\n        ]}\n        countriesSupported={[\n          \"US\",\n          \"CA\",\n          \"GB\",\n          \"AU\",\n          \"NZ\",\n          \"JP\",\n          \"KR\",\n          \"TW\",\n          \"HK\",\n          \"SG\",\n          \"MY\",\n          \"TH\",\n          \"PH\",\n          \"ID\",\n          \"VN\",\n          \"DE\",\n          \"FR\",\n          \"ES\",\n          \"IT\",\n          \"NL\",\n          \"SE\",\n          \"NO\",\n          \"DK\",\n          \"FI\",\n          \"BR\",\n          \"MX\",\n          \"AR\",\n        ]}\n      />\n\n      <div className=\"max-w-4xl\">\n        <div className=\"relative mb-8\">\n          <div className=\"absolute inset-0 bg-gradient-to-r from-purple-600 to-blue-600 rounded-lg opacity-10\"></div>\n          <div className=\"relative p-8\">\n            <div className=\"flex items-center space-x-6 mb-6\">\n              <div className=\"w-28 h-28 bg-gradient-to-br from-purple-500 to-blue-600 rounded-3xl flex items-center justify-center shadow-xl\">\n                <span className=\"text-white text-4xl font-bold\">DQ</span>\n              </div>\n              <div>\n                <h1 className=\"text-5xl font-bold mb-2\">\n                  Dragon Quest Legends\n                </h1>\n                <p className=\"text-xl text-gray-600\">\n                  The ultimate mobile RPG experience\n                </p>\n              </div>\n            </div>\n            <div className=\"flex flex-wrap gap-4\">\n              <div className=\"bg-green-100 text-green-800 px-4 py-2 rounded-full font-medium\">\n                RPG\n              </div>\n              <div className=\"bg-blue-100 text-blue-800 px-4 py-2 rounded-full font-medium\">\n                Multiplayer\n              </div>\n              <div className=\"bg-purple-100 text-purple-800 px-4 py-2 rounded-full font-medium\">\n                Adventure\n              </div>\n              <div className=\"bg-orange-100 text-orange-800 px-4 py-2 rounded-full font-medium\">\n                4+ Age Rating\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"bg-gradient-to-r from-purple-600 to-blue-600 text-white rounded-lg p-8 mb-8\">\n          <div className=\"flex items-center justify-between mb-6\">\n            <div>\n              <h2 className=\"text-3xl font-bold mb-2\">Available Now</h2>\n              <p className=\"text-xl opacity-90\">\n                Join millions of players worldwide!\n              </p>\n            </div>\n            <div className=\"text-right\">\n              <div className=\"text-4xl font-bold\">$19.99</div>\n              <div className=\"text-sm opacity-75\">One-time purchase</div>\n            </div>\n          </div>\n          <div className=\"flex flex-wrap gap-4\">\n            <a\n              href=\"#\"\n              className=\"inline-flex items-center bg-black text-white px-6 py-3 rounded-lg hover:bg-gray-800 transition\"\n            >\n              <svg\n                className=\"w-6 h-6 mr-2\"\n                viewBox=\"0 0 24 24\"\n                fill=\"currentColor\"\n              >\n                <path d=\"M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z\" />\n              </svg>\n              App Store\n            </a>\n            <a\n              href=\"#\"\n              className=\"inline-flex items-center bg-black text-white px-6 py-3 rounded-lg hover:bg-gray-800 transition\"\n            >\n              <svg\n                className=\"w-6 h-6 mr-2\"\n                viewBox=\"0 0 24 24\"\n                fill=\"currentColor\"\n              >\n                <path d=\"M3.609 1.814L13.792 12 3.61 22.186a.996.996 0 0 1-.61-.92V2.734a1 1 0 0 1 .609-.92zm10.89 10.893l2.302 2.302-10.937 6.333 8.635-8.635zm3.199-3.198l2.807 1.626a1 1 0 0 1 0 1.73l-2.808 1.626L15.206 12l2.492-2.491zM5.864 2.658L16.802 8.99l-2.303 2.303-8.635-8.635z\" />\n              </svg>\n              Google Play\n            </a>\n            <a\n              href=\"#\"\n              className=\"inline-flex items-center bg-red-600 text-white px-6 py-3 rounded-lg hover:bg-red-700 transition\"\n            >\n              <span className=\"mr-2\">🎮</span>\n              Nintendo eShop\n            </a>\n          </div>\n        </div>\n\n        <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4 mb-8\">\n          <div className=\"bg-white rounded-lg shadow-lg p-6 text-center\">\n            <div className=\"text-yellow-400 text-2xl mb-2\">★★★★★</div>\n            <div className=\"text-3xl font-bold\">4.8/5</div>\n            <div className=\"text-gray-600\">45K ratings</div>\n          </div>\n          <div className=\"bg-white rounded-lg shadow-lg p-6 text-center\">\n            <div className=\"text-purple-600 text-3xl font-bold mb-2\">10M+</div>\n            <div className=\"text-gray-600\">Downloads</div>\n          </div>\n          <div className=\"bg-white rounded-lg shadow-lg p-6 text-center\">\n            <div className=\"text-green-600 text-3xl font-bold mb-2\">#1</div>\n            <div className=\"text-gray-600\">RPG Game</div>\n          </div>\n          <div className=\"bg-white rounded-lg shadow-lg p-6 text-center\">\n            <div className=\"text-blue-600 text-3xl font-bold mb-2\">4.5GB</div>\n            <div className=\"text-gray-600\">Install size</div>\n          </div>\n        </div>\n\n        <section className=\"mb-12\">\n          <h2 className=\"text-3xl font-bold mb-6\">Game Features</h2>\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n            <div className=\"bg-gradient-to-br from-purple-50 to-blue-50 rounded-lg p-6\">\n              <h3 className=\"text-xl font-semibold mb-3 flex items-center\">\n                <span className=\"text-2xl mr-2\">⚔️</span>\n                Epic Storyline\n              </h3>\n              <p className=\"text-gray-700\">\n                Embark on a 50+ hour journey through a beautifully crafted\n                fantasy world. Make choices that shape your destiny and uncover\n                the secrets of the ancient dragons.\n              </p>\n            </div>\n            <div className=\"bg-gradient-to-br from-green-50 to-emerald-50 rounded-lg p-6\">\n              <h3 className=\"text-xl font-semibold mb-3 flex items-center\">\n                <span className=\"text-2xl mr-2\">🎮</span>\n                Real-Time Combat\n              </h3>\n              <p className=\"text-gray-700\">\n                Master our innovative real-time combat system with combo\n                attacks, skill chains, and strategic positioning. No turn-based\n                waiting!\n              </p>\n            </div>\n            <div className=\"bg-gradient-to-br from-orange-50 to-red-50 rounded-lg p-6\">\n              <h3 className=\"text-xl font-semibold mb-3 flex items-center\">\n                <span className=\"text-2xl mr-2\">👥</span>\n                Multiplayer Adventures\n              </h3>\n              <p className=\"text-gray-700\">\n                Team up with friends for epic raid battles, compete in PvP\n                tournaments, or trade rare items in the global marketplace.\n              </p>\n            </div>\n            <div className=\"bg-gradient-to-br from-pink-50 to-purple-50 rounded-lg p-6\">\n              <h3 className=\"text-xl font-semibold mb-3 flex items-center\">\n                <span className=\"text-2xl mr-2\">🐉</span>\n                Monster Collection\n              </h3>\n              <p className=\"text-gray-700\">\n                Capture and train over 300 unique monsters, each with their own\n                abilities and evolution paths. Build the ultimate team!\n              </p>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-12\">\n          <h2 className=\"text-3xl font-bold mb-6\">Screenshots</h2>\n          <div className=\"grid grid-cols-2 gap-4\">\n            <div className=\"bg-gray-200 rounded-lg aspect-video flex items-center justify-center shadow-lg\">\n              <span className=\"text-gray-500\">Combat System</span>\n            </div>\n            <div className=\"bg-gray-200 rounded-lg aspect-video flex items-center justify-center shadow-lg\">\n              <span className=\"text-gray-500\">Open World</span>\n            </div>\n            <div className=\"bg-gray-200 rounded-lg aspect-video flex items-center justify-center shadow-lg\">\n              <span className=\"text-gray-500\">Multiplayer Raid</span>\n            </div>\n            <div className=\"bg-gray-200 rounded-lg aspect-video flex items-center justify-center shadow-lg\">\n              <span className=\"text-gray-500\">Character Customization</span>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-12\">\n          <h2 className=\"text-2xl font-semibold mb-4\">System Requirements</h2>\n          <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <h3 className=\"font-semibold mb-3 flex items-center\">\n                <span className=\"text-2xl mr-2\">📱</span>\n                iOS\n              </h3>\n              <ul className=\"text-gray-700 space-y-1 text-sm\">\n                <li>• iOS 14.0 or later</li>\n                <li>• iPhone 8 or newer</li>\n                <li>• 3GB RAM minimum</li>\n                <li>• 4.5GB storage</li>\n              </ul>\n            </div>\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <h3 className=\"font-semibold mb-3 flex items-center\">\n                <span className=\"text-2xl mr-2\">🤖</span>\n                Android\n              </h3>\n              <ul className=\"text-gray-700 space-y-1 text-sm\">\n                <li>• Android 9.0 or higher</li>\n                <li>• 3GB RAM minimum</li>\n                <li>• Snapdragon 665 or better</li>\n                <li>• 4.5GB storage</li>\n              </ul>\n            </div>\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <h3 className=\"font-semibold mb-3 flex items-center\">\n                <span className=\"text-2xl mr-2\">🎮</span>\n                Nintendo Switch\n              </h3>\n              <ul className=\"text-gray-700 space-y-1 text-sm\">\n                <li>• All Switch models</li>\n                <li>• 5GB storage</li>\n                <li>• Internet for multiplayer</li>\n                <li>• Nintendo Online subscription</li>\n              </ul>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-12\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Recent Reviews</h2>\n          <div className=\"space-y-4\">\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <div className=\"flex items-center justify-between mb-2\">\n                <div className=\"flex items-center\">\n                  <span className=\"font-semibold text-lg\">\n                    GamePro Magazine\n                  </span>\n                  <span className=\"ml-2 text-yellow-400\">★★★★★</span>\n                </div>\n                <span className=\"text-gray-500 text-sm\">October 15, 2024</span>\n              </div>\n              <p className=\"text-gray-700\">\n                \"A masterpiece of mobile gaming. Dragon Quest Legends sets a new\n                standard for RPGs on mobile platforms.\"\n              </p>\n            </div>\n            <div className=\"bg-white rounded-lg shadow p-6\">\n              <div className=\"flex items-center justify-between mb-2\">\n                <div className=\"flex items-center\">\n                  <span className=\"font-semibold text-lg\">Jessica Chen</span>\n                  <span className=\"ml-2 text-yellow-400\">★★★★☆</span>\n                </div>\n                <span className=\"text-gray-500 text-sm\">November 1, 2024</span>\n              </div>\n              <p className=\"text-gray-700\">\n                \"Amazing graphics and gameplay! The story is captivating, though\n                the game requires a constant internet connection.\"\n              </p>\n            </div>\n          </div>\n        </section>\n\n        <section>\n          <div className=\"bg-gradient-to-r from-purple-100 to-blue-100 rounded-lg p-8 text-center\">\n            <h2 className=\"text-3xl font-bold mb-4\">Ready for Adventure?</h2>\n            <p className=\"text-gray-700 mb-6\">\n              Join the Dragon Quest Legends community and start your epic\n              journey today!\n            </p>\n            <button className=\"bg-gradient-to-r from-purple-600 to-blue-600 text-white px-8 py-4 rounded-lg text-lg font-semibold hover:from-purple-700 hover:to-blue-700 transition\">\n              Download Now - $19.99\n            </button>\n          </div>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/video-live/page.tsx",
    "content": "import { VideoJsonLd } from \"next-seo\";\n\nexport default function VideoLivePage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <VideoJsonLd\n        name=\"Live Cooking Show: New Year's Eve Special\"\n        description=\"Join us for a live cooking demonstration preparing a complete New Year's Eve feast\"\n        thumbnailUrl=\"https://example.com/live-show-nye-thumbnail.jpg\"\n        uploadDate=\"2024-12-20T10:00:00+00:00\"\n        embedUrl=\"https://example.com/live/nye-special\"\n        interactionStatistic={{\n          interactionType: \"WatchAction\",\n          userInteractionCount: 15000,\n        }}\n        publication={[\n          {\n            name: \"First Broadcast\",\n            isLiveBroadcast: true,\n            startDate: \"2024-12-31T20:00:00+00:00\",\n            endDate: \"2024-12-31T22:00:00+00:00\",\n          },\n          {\n            name: \"Encore Presentation\",\n            isLiveBroadcast: true,\n            startDate: \"2025-01-01T14:00:00+00:00\",\n            endDate: \"2025-01-01T16:00:00+00:00\",\n          },\n        ]}\n        author=\"Chef Maria Rodriguez\"\n        publisher={{\n          name: \"Live Cooking Network\",\n          logo: \"https://example.com/live-cooking-network-logo.png\",\n        }}\n      />\n\n      <main className=\"prose lg:prose-xl mx-auto\">\n        <h1>Live Cooking Show: New Year's Eve Special</h1>\n\n        <div className=\"bg-red-50 border-2 border-red-200 p-6 rounded-lg mb-8\">\n          <div className=\"flex items-center gap-3 mb-4\">\n            <div className=\"w-4 h-4 bg-red-600 rounded-full animate-pulse\"></div>\n            <span className=\"text-xl font-bold text-red-600\">\n              LIVE BROADCAST\n            </span>\n          </div>\n          <p className=\"text-lg mb-4\">\n            Join us for a special New Year's Eve cooking demonstration!\n          </p>\n          <div className=\"space-y-2\">\n            <p>\n              <strong>First Broadcast:</strong> December 31, 2024 at 8:00 PM UTC\n            </p>\n            <p>\n              <strong>Encore:</strong> January 1, 2025 at 2:00 PM UTC\n            </p>\n          </div>\n        </div>\n\n        <div className=\"aspect-w-16 aspect-h-9 mb-8\">\n          <div className=\"bg-gray-200 rounded-lg flex items-center justify-center\">\n            <p className=\"text-xl text-gray-600\">\n              Live stream player will appear here\n            </p>\n          </div>\n        </div>\n\n        <section>\n          <h2>About This Live Show</h2>\n          <p>\n            Join Chef Maria Rodriguez for an exciting live cooking demonstration\n            where she'll prepare a complete New Year's Eve feast. From\n            appetizers to desserts, learn how to create the perfect celebration\n            menu.\n          </p>\n        </section>\n\n        <section>\n          <h2>What We'll Be Making</h2>\n          <ul>\n            <li>Champagne-poached shrimp appetizers</li>\n            <li>Herb-crusted prime rib with au jus</li>\n            <li>Truffle mashed potatoes</li>\n            <li>Roasted winter vegetables</li>\n            <li>Chocolate lava cake with gold leaf</li>\n          </ul>\n        </section>\n\n        <section>\n          <h2>Live Interaction</h2>\n          <p>\n            During the live broadcast, Chef Maria will answer your questions in\n            real-time. Submit your questions through the chat and she'll address\n            them throughout the show.\n          </p>\n        </section>\n\n        <div className=\"bg-blue-50 p-6 rounded-lg\">\n          <h3 className=\"text-lg font-semibold mb-2\">Current Viewers</h3>\n          <p className=\"text-3xl font-bold\">15,000+</p>\n          <p className=\"text-gray-600\">People watching</p>\n        </div>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/video-seekto/page.tsx",
    "content": "import { VideoJsonLd } from \"next-seo\";\n\nexport default function VideoSeekToPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <VideoJsonLd\n        name=\"50 Quick Kitchen Tips & Tricks\"\n        description=\"A rapid-fire collection of 50 essential kitchen tips and tricks that will transform your cooking\"\n        thumbnailUrl={[\n          \"https://example.com/kitchen-tips-1x1.jpg\",\n          \"https://example.com/kitchen-tips-16x9.jpg\",\n        ]}\n        uploadDate=\"2024-04-01T12:00:00+00:00\"\n        embedUrl=\"https://example.com/embed/kitchen-tips\"\n        duration=\"PT25M\"\n        interactionStatistic={{\n          interactionType: \"WatchAction\",\n          userInteractionCount: 250000,\n        }}\n        potentialAction={{\n          target:\n            \"https://example.com/videos/kitchen-tips?t={seek_to_second_number}\",\n          \"startOffset-input\": \"required name=seek_to_second_number\",\n        }}\n        author=\"Chef Sarah Thompson\"\n        publisher={{\n          name: \"Quick Cooking Tips\",\n          logo: \"https://example.com/quick-cooking-tips-logo.png\",\n        }}\n      />\n\n      <main className=\"prose lg:prose-xl mx-auto\">\n        <h1>50 Quick Kitchen Tips & Tricks</h1>\n\n        <div className=\"bg-yellow-50 border-2 border-yellow-200 p-6 rounded-lg mb-8\">\n          <h3 className=\"text-lg font-semibold mb-2\">\n            🎯 Smart Navigation Enabled\n          </h3>\n          <p>\n            This video features automatic key moment detection. Search engines\n            can automatically identify and link to specific tips throughout the\n            video based on the content.\n          </p>\n        </div>\n\n        <section>\n          <h2>About This Video</h2>\n          <p>\n            Join Chef Sarah Thompson for a rapid-fire session of 50 essential\n            kitchen tips that will save you time, reduce waste, and improve your\n            cooking. Each tip is explained in just 30 seconds, making this the\n            ultimate quick reference guide.\n          </p>\n        </section>\n\n        <section>\n          <h2>How SeekToAction Works</h2>\n          <div className=\"bg-blue-50 p-6 rounded-lg\">\n            <p className=\"mb-4\">\n              This video uses <strong>SeekToAction</strong> structured data,\n              which allows search engines to:\n            </p>\n            <ul>\n              <li>Automatically detect key moments in the video</li>\n              <li>Create direct links to specific tips</li>\n              <li>Display relevant segments in search results</li>\n              <li>Enable users to jump directly to the content they need</li>\n            </ul>\n            <p className=\"mt-4 text-sm\">\n              <strong>URL Pattern:</strong>{\" \"}\n              <code>\n                ?t={\"{\"}seek_to_second_number{\"}\"}\n              </code>\n            </p>\n          </div>\n        </section>\n\n        <section>\n          <h2>Sample Tips Covered</h2>\n          <div className=\"grid md:grid-cols-2 gap-4\">\n            <div className=\"bg-gray-50 p-4 rounded-lg\">\n              <h3 className=\"font-semibold\">🥑 Avocado Tips</h3>\n              <p>How to perfectly ripen avocados and keep them fresh longer</p>\n            </div>\n            <div className=\"bg-gray-50 p-4 rounded-lg\">\n              <h3 className=\"font-semibold\">🔪 Knife Skills</h3>\n              <p>Professional techniques for faster, safer chopping</p>\n            </div>\n            <div className=\"bg-gray-50 p-4 rounded-lg\">\n              <h3 className=\"font-semibold\">🧄 Garlic Hacks</h3>\n              <p>Quick peeling methods and flavor maximization</p>\n            </div>\n            <div className=\"bg-gray-50 p-4 rounded-lg\">\n              <h3 className=\"font-semibold\">🍳 Pan Maintenance</h3>\n              <p>Keep your cookware in perfect condition</p>\n            </div>\n          </div>\n        </section>\n\n        <section>\n          <h2>Why This Format?</h2>\n          <p>\n            By presenting 50 tips in rapid succession, viewers can quickly find\n            exactly what they need. The SeekToAction implementation means search\n            engines can understand the content structure and help users jump\n            directly to relevant tips.\n          </p>\n        </section>\n\n        <div className=\"bg-green-50 p-6 rounded-lg\">\n          <p className=\"text-lg\">\n            <strong>Viewer Tip:</strong> Try searching for specific cooking\n            problems in your search engine. If this video contains a relevant\n            tip, you might see a direct link to that exact moment in the video!\n          </p>\n        </div>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/app/web-app/page.tsx",
    "content": "import { SoftwareApplicationJsonLd } from \"next-seo\";\n\nexport default function WebAppPage() {\n  return (\n    <div className=\"container mx-auto p-8\">\n      <SoftwareApplicationJsonLd\n        type=\"WebApplication\"\n        name=\"CloudSync Pro - Team Collaboration Platform\"\n        description=\"Real-time collaboration platform for modern teams with document sharing, video conferencing, and project management\"\n        url=\"https://app.cloudsyncpro.com\"\n        image={{\n          url: \"https://example.com/cloudsync-logo.svg\",\n          width: 512,\n          height: 512,\n        }}\n        applicationCategory=\"BusinessApplication\"\n        applicationSubCategory=\"TeamCollaboration\"\n        operatingSystem=\"Web browser (Chrome 90+, Firefox 88+, Safari 14+, Edge 90+)\"\n        offers={[\n          {\n            price: 0,\n            priceCurrency: \"USD\",\n            availability: \"https://schema.org/InStock\",\n            url: \"https://app.cloudsyncpro.com/signup/free\",\n          },\n          {\n            price: 12,\n            priceCurrency: \"USD\",\n            availability: \"https://schema.org/InStock\",\n            url: \"https://app.cloudsyncpro.com/signup/pro\",\n          },\n          {\n            price: 25,\n            priceCurrency: \"USD\",\n            availability: \"https://schema.org/InStock\",\n            url: \"https://app.cloudsyncpro.com/signup/enterprise\",\n          },\n        ]}\n        aggregateRating={{\n          ratingValue: 4.6,\n          ratingCount: 12500,\n          reviewCount: 8900,\n        }}\n        review={[\n          {\n            author: {\n              name: \"TechStartup Inc.\",\n              \"@type\": \"Organization\",\n            },\n            reviewRating: { ratingValue: 5 },\n            reviewBody:\n              \"CloudSync Pro transformed how our remote team collaborates. The real-time features are incredible!\",\n            datePublished: \"2024-11-05\",\n          },\n          {\n            author: \"David Kim\",\n            reviewRating: { ratingValue: 4 },\n            reviewBody:\n              \"Excellent features and reliability. Would benefit from more integrations with design tools.\",\n            datePublished: \"2024-10-20\",\n          },\n        ]}\n        screenshot={[\n          {\n            url: \"https://example.com/screenshots/cloudsync-dashboard.png\",\n            caption: \"Team dashboard with project overview\",\n          },\n          {\n            url: \"https://example.com/screenshots/cloudsync-editor.png\",\n            caption: \"Real-time collaborative document editor\",\n          },\n          {\n            url: \"https://example.com/screenshots/cloudsync-video.png\",\n            caption: \"Built-in video conferencing\",\n          },\n          {\n            url: \"https://example.com/screenshots/cloudsync-kanban.png\",\n            caption: \"Kanban board for project management\",\n          },\n        ]}\n        featureList={[\n          \"Real-time collaborative editing\",\n          \"HD video conferencing (up to 100 participants)\",\n          \"Unlimited cloud storage\",\n          \"Advanced project management tools\",\n          \"End-to-end encryption\",\n          \"API access for integrations\",\n          \"Mobile responsive design\",\n          \"Offline mode with sync\",\n          \"Two-factor authentication\",\n          \"Custom branding options\",\n        ]}\n        softwareVersion=\"8.5.3\"\n        datePublished=\"2018-09-01\"\n        dateModified=\"2024-11-25\"\n        author={{\n          name: \"CloudSync Technologies\",\n          url: \"https://cloudsynctech.com\",\n        }}\n        publisher={{\n          name: \"CloudSync Technologies Inc.\",\n          url: \"https://cloudsynctech.com\",\n          logo: {\n            url: \"https://cloudsynctech.com/press/logo.png\",\n            width: 600,\n            height: 60,\n          },\n          address: {\n            streetAddress: \"123 Tech Boulevard\",\n            addressLocality: \"San Francisco\",\n            addressRegion: \"CA\",\n            postalCode: \"94105\",\n            addressCountry: \"US\",\n          },\n        }}\n        countriesSupported={[\n          \"US\",\n          \"CA\",\n          \"GB\",\n          \"DE\",\n          \"FR\",\n          \"ES\",\n          \"IT\",\n          \"NL\",\n          \"SE\",\n          \"NO\",\n          \"DK\",\n          \"FI\",\n          \"AU\",\n          \"NZ\",\n          \"JP\",\n          \"SG\",\n          \"HK\",\n          \"IN\",\n          \"BR\",\n          \"MX\",\n        ]}\n      />\n\n      <div className=\"max-w-4xl\">\n        <div className=\"text-center mb-12\">\n          <div className=\"inline-flex items-center justify-center w-20 h-20 bg-blue-600 rounded-2xl mb-4\">\n            <span className=\"text-white text-3xl font-bold\">CS</span>\n          </div>\n          <h1 className=\"text-5xl font-bold mb-4\">CloudSync Pro</h1>\n          <p className=\"text-xl text-gray-600 mb-8\">\n            Where teams achieve more together\n          </p>\n          <div className=\"flex justify-center space-x-4\">\n            <a\n              href=\"#\"\n              className=\"bg-blue-600 text-white px-8 py-4 rounded-lg text-lg font-semibold hover:bg-blue-700 transition\"\n            >\n              Start Free Trial\n            </a>\n            <a\n              href=\"#\"\n              className=\"border-2 border-gray-300 text-gray-700 px-8 py-4 rounded-lg text-lg font-semibold hover:border-gray-400 transition\"\n            >\n              View Demo\n            </a>\n          </div>\n        </div>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6 mb-12\">\n          <div className=\"bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg p-6 text-center\">\n            <h3 className=\"text-4xl font-bold text-blue-600 mb-2\">Free</h3>\n            <p className=\"text-gray-600 mb-4\">\n              For individuals and small teams\n            </p>\n            <ul className=\"text-left space-y-2 text-sm\">\n              <li>✓ Up to 5 team members</li>\n              <li>✓ 10GB storage</li>\n              <li>✓ Basic features</li>\n            </ul>\n          </div>\n          <div className=\"bg-gradient-to-br from-purple-50 to-pink-50 rounded-lg p-6 text-center border-2 border-purple-200\">\n            <h3 className=\"text-4xl font-bold text-purple-600 mb-2\">\n              $12<span className=\"text-lg\">/user/mo</span>\n            </h3>\n            <p className=\"text-gray-600 mb-4\">For growing teams</p>\n            <ul className=\"text-left space-y-2 text-sm\">\n              <li>✓ Unlimited team members</li>\n              <li>✓ 1TB storage per user</li>\n              <li>✓ Advanced features</li>\n              <li>✓ Priority support</li>\n            </ul>\n          </div>\n          <div className=\"bg-gradient-to-br from-green-50 to-emerald-50 rounded-lg p-6 text-center\">\n            <h3 className=\"text-4xl font-bold text-green-600 mb-2\">\n              $25<span className=\"text-lg\">/user/mo</span>\n            </h3>\n            <p className=\"text-gray-600 mb-4\">For enterprises</p>\n            <ul className=\"text-left space-y-2 text-sm\">\n              <li>✓ Everything in Pro</li>\n              <li>✓ Unlimited storage</li>\n              <li>✓ Custom integrations</li>\n              <li>✓ Dedicated support</li>\n              <li>✓ SLA guarantee</li>\n            </ul>\n          </div>\n        </div>\n\n        <section className=\"mb-12\">\n          <h2 className=\"text-3xl font-bold text-center mb-8\">\n            Everything your team needs in one place\n          </h2>\n          <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n            <div className=\"bg-white rounded-lg shadow-lg p-6\">\n              <div className=\"text-4xl mb-4\">📝</div>\n              <h3 className=\"text-xl font-semibold mb-2\">\n                Real-time Collaboration\n              </h3>\n              <p className=\"text-gray-600\">\n                Work together on documents, spreadsheets, and presentations in\n                real-time\n              </p>\n            </div>\n            <div className=\"bg-white rounded-lg shadow-lg p-6\">\n              <div className=\"text-4xl mb-4\">📹</div>\n              <h3 className=\"text-xl font-semibold mb-2\">Video Conferencing</h3>\n              <p className=\"text-gray-600\">\n                HD video calls with screen sharing, recording, and up to 100\n                participants\n              </p>\n            </div>\n            <div className=\"bg-white rounded-lg shadow-lg p-6\">\n              <div className=\"text-4xl mb-4\">📊</div>\n              <h3 className=\"text-xl font-semibold mb-2\">Project Management</h3>\n              <p className=\"text-gray-600\">\n                Kanban boards, Gantt charts, and agile tools to keep projects on\n                track\n              </p>\n            </div>\n            <div className=\"bg-white rounded-lg shadow-lg p-6\">\n              <div className=\"text-4xl mb-4\">🔒</div>\n              <h3 className=\"text-xl font-semibold mb-2\">\n                Enterprise Security\n              </h3>\n              <p className=\"text-gray-600\">\n                End-to-end encryption, SSO, 2FA, and compliance with SOC 2 and\n                GDPR\n              </p>\n            </div>\n            <div className=\"bg-white rounded-lg shadow-lg p-6\">\n              <div className=\"text-4xl mb-4\">🔌</div>\n              <h3 className=\"text-xl font-semibold mb-2\">1000+ Integrations</h3>\n              <p className=\"text-gray-600\">\n                Connect with Slack, GitHub, Jira, Salesforce, and all your\n                favorite tools\n              </p>\n            </div>\n            <div className=\"bg-white rounded-lg shadow-lg p-6\">\n              <div className=\"text-4xl mb-4\">📱</div>\n              <h3 className=\"text-xl font-semibold mb-2\">Works Everywhere</h3>\n              <p className=\"text-gray-600\">\n                Access from any browser, with dedicated apps for iOS and Android\n              </p>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-12\">\n          <h2 className=\"text-3xl font-bold text-center mb-8\">\n            Trusted by teams worldwide\n          </h2>\n          <div className=\"bg-gray-50 rounded-lg p-8\">\n            <div className=\"grid grid-cols-2 md:grid-cols-4 gap-8 items-center\">\n              <div className=\"text-center\">\n                <div className=\"text-4xl font-bold text-blue-600\">4.6/5</div>\n                <div className=\"text-yellow-400\">★★★★★</div>\n                <div className=\"text-gray-600\">12,500+ reviews</div>\n              </div>\n              <div className=\"text-center\">\n                <div className=\"text-4xl font-bold text-green-600\">99.9%</div>\n                <div className=\"text-gray-600\">Uptime SLA</div>\n              </div>\n              <div className=\"text-center\">\n                <div className=\"text-4xl font-bold text-purple-600\">50K+</div>\n                <div className=\"text-gray-600\">Active teams</div>\n              </div>\n              <div className=\"text-center\">\n                <div className=\"text-4xl font-bold text-orange-600\">2M+</div>\n                <div className=\"text-gray-600\">Daily users</div>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        <section className=\"mb-12\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Browser Requirements</h2>\n          <div className=\"bg-white rounded-lg shadow p-6\">\n            <p className=\"text-gray-700 mb-4\">\n              CloudSync Pro works best with modern browsers. For optimal\n              performance, please use:\n            </p>\n            <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n              <div className=\"text-center\">\n                <div className=\"text-3xl mb-2\">🌐</div>\n                <p className=\"font-medium\">Chrome</p>\n                <p className=\"text-sm text-gray-600\">Version 90+</p>\n              </div>\n              <div className=\"text-center\">\n                <div className=\"text-3xl mb-2\">🦊</div>\n                <p className=\"font-medium\">Firefox</p>\n                <p className=\"text-sm text-gray-600\">Version 88+</p>\n              </div>\n              <div className=\"text-center\">\n                <div className=\"text-3xl mb-2\">🧭</div>\n                <p className=\"font-medium\">Safari</p>\n                <p className=\"text-sm text-gray-600\">Version 14+</p>\n              </div>\n              <div className=\"text-center\">\n                <div className=\"text-3xl mb-2\">🔷</div>\n                <p className=\"font-medium\">Edge</p>\n                <p className=\"text-sm text-gray-600\">Version 90+</p>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        <section>\n          <h2 className=\"text-2xl font-semibold mb-4\">Screenshots</h2>\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n            <div className=\"bg-gray-200 rounded-lg aspect-video flex items-center justify-center\">\n              <span className=\"text-gray-500\">Team Dashboard</span>\n            </div>\n            <div className=\"bg-gray-200 rounded-lg aspect-video flex items-center justify-center\">\n              <span className=\"text-gray-500\">Document Editor</span>\n            </div>\n            <div className=\"bg-gray-200 rounded-lg aspect-video flex items-center justify-center\">\n              <span className=\"text-gray-500\">Video Conference</span>\n            </div>\n            <div className=\"bg-gray-200 rounded-lg aspect-video flex items-center justify-center\">\n              <span className=\"text-gray-500\">Project Board</span>\n            </div>\n          </div>\n        </section>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/app-router-showcase/components/custom/PodcastSeriesJsonLd.tsx",
    "content": "\"use client\";\n\nimport { JsonLdScript, processors } from \"next-seo\";\n\ninterface PodcastEpisode {\n  name: string;\n  duration?: string;\n  datePublished?: string;\n  description?: string;\n  url?: string;\n}\n\ninterface PodcastSeriesJsonLdProps {\n  name: string;\n  description?: string;\n  host?: string | { name: string; url?: string };\n  episodes?: PodcastEpisode[];\n  image?: string | { url: string; width?: number; height?: number };\n  url?: string;\n}\n\n/**\n * Custom PodcastSeries JSON-LD component built using next-seo's core utilities\n * Demonstrates how to create custom structured data components with the library's processors\n */\nexport function PodcastSeriesJsonLd({\n  name,\n  description,\n  host,\n  episodes,\n  image,\n  url,\n}: PodcastSeriesJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"PodcastSeries\",\n    name,\n    ...(description && { description }),\n    ...(url && { url }),\n    ...(host && {\n      host:\n        typeof host === \"string\"\n          ? processors.processAuthor(host)\n          : processors.processAuthor(host),\n    }),\n    ...(image && { image: processors.processImage(image) }),\n    ...(episodes &&\n      episodes.length > 0 && {\n        episode: episodes.map((ep, index) => ({\n          \"@type\": \"PodcastEpisode\",\n          name: ep.name,\n          position: index + 1,\n          ...(ep.duration && { duration: ep.duration }),\n          ...(ep.datePublished && { datePublished: ep.datePublished }),\n          ...(ep.description && { description: ep.description }),\n          ...(ep.url && { url: ep.url }),\n        })),\n      }),\n  };\n\n  return <JsonLdScript data={data} scriptKey=\"podcast-series\" />;\n}\n"
  },
  {
    "path": "examples/app-router-showcase/components/custom/ServiceJsonLd.tsx",
    "content": "\"use client\";\n\nimport { JsonLdScript, processors, type Organization } from \"next-seo\";\n\ninterface ServiceJsonLdProps {\n  name: string;\n  serviceType?: string;\n  provider?: string | Omit<Organization, \"@type\">;\n  areaServed?: string | string[];\n  description?: string;\n  url?: string;\n  offers?: {\n    price?: number;\n    priceCurrency?: string;\n    priceRange?: string;\n  };\n  aggregateRating?: {\n    ratingValue: number;\n    reviewCount?: number;\n    bestRating?: number;\n    worstRating?: number;\n  };\n}\n\n/**\n * Custom Service JSON-LD component built using next-seo's core utilities\n * Demonstrates flexible input processing and the @type optional pattern\n */\nexport function ServiceJsonLd({\n  name,\n  serviceType,\n  provider,\n  areaServed,\n  description,\n  url,\n  offers,\n  aggregateRating,\n}: ServiceJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Service\",\n    name,\n    ...(serviceType && { serviceType }),\n    ...(description && { description }),\n    ...(url && { url }),\n    ...(provider && {\n      provider:\n        typeof provider === \"string\"\n          ? processors.processOrganization(provider)\n          : processors.processOrganization(provider),\n    }),\n    ...(areaServed && {\n      areaServed: Array.isArray(areaServed) ? areaServed : [areaServed],\n    }),\n    ...(offers && {\n      offers: {\n        \"@type\": \"Offer\",\n        ...(offers.price && { price: offers.price }),\n        ...(offers.priceCurrency && { priceCurrency: offers.priceCurrency }),\n        ...(offers.priceRange && { priceRange: offers.priceRange }),\n      },\n    }),\n    ...(aggregateRating && {\n      aggregateRating: processors.processAggregateRating(aggregateRating),\n    }),\n  };\n\n  return <JsonLdScript data={data} scriptKey=\"service\" />;\n}\n"
  },
  {
    "path": "examples/app-router-showcase/eslint.config.mjs",
    "content": "import { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { FlatCompat } from \"@eslint/eslintrc\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst compat = new FlatCompat({\n  baseDirectory: __dirname,\n});\n\nconst eslintConfig = [\n  ...compat.extends(\"next/core-web-vitals\", \"next/typescript\"),\n  {\n    rules: {\n      \"react/no-unescaped-entities\": \"off\",\n    },\n  },\n];\n\nexport default eslintConfig;\n"
  },
  {
    "path": "examples/app-router-showcase/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  /* config options here */\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/app-router-showcase/package.json",
    "content": "{\n  \"name\": \"app-router-showcase\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"next\": \"15.3.2\",\n    \"next-seo\": \"workspace:^\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"15.5.6\",\n    \"typescript\": \"^5\"\n  }\n}\n"
  },
  {
    "path": "examples/app-router-showcase/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n      \"~/*\": [\"../../src/*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"next-seo\",\n  \"version\": \"7.2.0\",\n  \"description\": \"SEO plugin for Next.js projects\",\n  \"sideEffects\": false,\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/garmeeh/next-seo.git\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./pages\": {\n      \"types\": \"./dist/pages.d.ts\",\n      \"import\": \"./dist/pages.mjs\",\n      \"require\": \"./dist/pages.js\"\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"// BUILD & DEVELOPMENT\": \"\",\n    \"dev\": \"tsup --watch\",\n    \"build\": \"tsup\",\n    \"// CODE QUALITY & FORMATTING\": \"\",\n    \"lint\": \"eslint . --ext .ts,.tsx\",\n    \"lint:fix\": \"eslint . --ext .ts,.tsx --fix\",\n    \"format\": \"prettier --write \\\"**/*.{ts,tsx,js,jsx,json,md}\\\"\",\n    \"typecheck\": \"tsc --noEmit --project tsconfig.json\",\n    \"// TESTING\": \"\",\n    \"test\": \"pnpm typecheck && pnpm lint && echo '> Linting Complete. To run tests use the following commands:\\n\\n> Unit tests: pnpm test:unit \\n> E2E tests: pnpm test:e2e'\",\n    \"test:unit\": \"vitest run\",\n    \"test:unit:watch\": \"vitest\",\n    \"test:e2e\": \"playwright test\",\n    \"test:e2e:ui\": \"playwright test --ui\",\n    \"test:sweep\": \"pnpm build && pnpm typecheck && pnpm lint && pnpm example:lint && pnpm example:typecheck && pnpm test:unit && pnpm test:e2e\",\n    \"coverage\": \"vitest run --coverage\",\n    \"// UTILITIES\": \"\",\n    \"clean\": \"rimraf dist coverage examples/app-router-showcase/.next examples/app-router-showcase/out\",\n    \"prepare\": \"husky\",\n    \"// EXAMPLE APP (if you want shortcuts from root)\": \"\",\n    \"example:dev\": \"pnpm --filter ./examples/app-router-showcase dev\",\n    \"example:build\": \"pnpm --filter ./examples/app-router-showcase build\",\n    \"example:start\": \"pnpm --filter ./examples/app-router-showcase start\",\n    \"example:lint\": \"pnpm --filter ./examples/app-router-showcase lint\",\n    \"example:typecheck\": \"tsc --noEmit --project examples/app-router-showcase\",\n    \"// PUBLISHING\": \"\",\n    \"prepublishOnly\": \"pnpm build && pnpm test\",\n    \"changeset\": \"changeset\",\n    \"version-packages\": \"changeset version\",\n    \"release\": \"pnpm build && changeset publish\"\n  },\n  \"lint-staged\": {\n    \"*.{ts,tsx}\": \"eslint --fix\",\n    \"*.{ts,tsx,js,jsx,json,md}\": \"prettier --write\"\n  },\n  \"devDependencies\": {\n    \"@changesets/cli\": \"^2.29.7\",\n    \"@eslint/js\": \"^9.38.0\",\n    \"@playwright/test\": \"^1.56.1\",\n    \"@testing-library/jest-dom\": \"^6.9.1\",\n    \"@testing-library/react\": \"^16.3.0\",\n    \"@types/node\": \"^24.8.1\",\n    \"@types/react\": \"^19.2.2\",\n    \"@vitejs/plugin-react\": \"^5.0.4\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"ajv\": \"^8.17.1\",\n    \"eslint\": \"^9.38.0\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"globals\": \"^16.4.0\",\n    \"husky\": \"^9.1.7\",\n    \"jsdom\": \"^27.0.1\",\n    \"lint-staged\": \"^16.2.4\",\n    \"prettier\": \"^3.6.2\",\n    \"rimraf\": \"^6.0.1\",\n    \"tsup\": \"^8.5.0\",\n    \"typescript\": \"^5.9.3\",\n    \"typescript-eslint\": \"^8.46.1\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"next\": \">=13.4.0\",\n    \"react\": \">=18.2.0\"\n  }\n}\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import { defineConfig, devices } from \"@playwright/test\";\n\nexport default defineConfig({\n  testDir: \"./tests/e2e\",\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  workers: process.env.CI ? 1 : undefined,\n  reporter: \"html\",\n  use: {\n    baseURL: \"http://localhost:3001\", // << MAKE SURE THIS MATCHES YOUR EXAMPLE APP'S PORT\n    trace: \"on-first-retry\",\n  },\n  projects: [{ name: \"chromium\", use: { ...devices[\"Desktop Chrome\"] } }],\n  webServer: {\n    // Command to start your example app\n    command: process.env.CI\n      ? \"pnpm --filter ./examples/app-router-showcase start --port 3001\" // Production build in CI\n      : \"pnpm --filter ./examples/app-router-showcase dev --port 3001\", // Dev server locally\n    url: \"http://localhost:3001\", // << MAKE SURE THIS MATCHES\n    reuseExistingServer: !process.env.CI,\n    stdout: \"pipe\",\n    stderr: \"pipe\",\n    timeout: 120 * 1000, // Increase timeout for webServer to start\n  },\n});\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - '.'\n  - 'examples/*'"
  },
  {
    "path": "repomix.config.json",
    "content": "{\n  \"output\": {\n    \"style\": \"xml\",\n    \"filePath\": \"repomix-output.xml\"\n  },\n  \"ignore\": {\n    \"customPatterns\": [\n      \"playwright-report/\",\n      \"node_modules/\",\n      \"test-results/\",\n      \"dist/\",\n      \"coverage/\",\n      \"pnpm-lock.yaml\",\n      \"examples/\"\n    ]\n  }\n}\n"
  },
  {
    "path": "src/components/.gitkeep",
    "content": " "
  },
  {
    "path": "src/components/AggregateRatingJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport AggregateRatingJsonLd from \"./AggregateRatingJsonLd\";\n\ndescribe(\"AggregateRatingJsonLd\", () => {\n  it(\"renders minimal with ratingCount\", () => {\n    const { container } = render(\n      <AggregateRatingJsonLd\n        itemReviewed=\"Legal Seafood\"\n        ratingValue={88}\n        ratingCount={20}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"AggregateRating\",\n      itemReviewed: { \"@type\": \"Thing\", name: \"Legal Seafood\" },\n      ratingValue: 88,\n      ratingCount: 20,\n    });\n  });\n\n  it(\"renders with reviewCount instead of ratingCount\", () => {\n    const { container } = render(\n      <AggregateRatingJsonLd\n        itemReviewed={{ name: \"Executive Anvil\", \"@type\": \"Product\" }}\n        ratingValue={4.4}\n        reviewCount={89}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.reviewCount).toBe(89);\n    expect(jsonData.ratingCount).toBeUndefined();\n    expect(jsonData.itemReviewed).toEqual({\n      \"@type\": \"Product\",\n      name: \"Executive Anvil\",\n    });\n  });\n\n  it(\"includes custom scale when provided\", () => {\n    const { container } = render(\n      <AggregateRatingJsonLd\n        itemReviewed=\"Item\"\n        ratingValue={85}\n        ratingCount={100}\n        bestRating={100}\n        worstRating={0}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.bestRating).toBe(100);\n    expect(jsonData.worstRating).toBe(0);\n  });\n\n  it(\"throws when missing ratingCount and reviewCount\", () => {\n    expect(() =>\n      render(<AggregateRatingJsonLd itemReviewed=\"Item\" ratingValue={4} />),\n    ).toThrow(\n      \"AggregateRating requires at least one of ratingCount or reviewCount\",\n    );\n  });\n});\n"
  },
  {
    "path": "src/components/AggregateRatingJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { AggregateRatingJsonLdProps } from \"~/types/review.types\";\nimport { processItemReviewed } from \"~/utils/processors\";\n\nexport default function AggregateRatingJsonLd({\n  scriptId,\n  scriptKey,\n  itemReviewed,\n  ratingValue,\n  ratingCount,\n  reviewCount,\n  bestRating,\n  worstRating,\n}: AggregateRatingJsonLdProps) {\n  if (!itemReviewed) {\n    throw new Error(\n      \"AggregateRating requires itemReviewed when used standalone\",\n    );\n  }\n  if (!ratingCount && !reviewCount) {\n    throw new Error(\n      \"AggregateRating requires at least one of ratingCount or reviewCount\",\n    );\n  }\n\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"AggregateRating\",\n    itemReviewed: processItemReviewed(itemReviewed),\n    ratingValue,\n    ...(ratingCount !== undefined && { ratingCount }),\n    ...(reviewCount !== undefined && { reviewCount }),\n    ...(bestRating !== undefined && { bestRating }),\n    ...(worstRating !== undefined && { worstRating }),\n  } as const;\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"aggregaterating-jsonld\"}\n    />\n  );\n}\n\nexport type { AggregateRatingJsonLdProps };\n"
  },
  {
    "path": "src/components/ArticleJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport ArticleJsonLd from \"./ArticleJsonLd\";\n\ndescribe(\"ArticleJsonLd\", () => {\n  it(\"renders basic Article with minimal props\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Article\",\n      headline: \"Test Article\",\n      datePublished: \"2024-01-01T00:00:00.000Z\",\n      dateModified: \"2024-01-01T00:00:00.000Z\", // defaults to datePublished\n    });\n  });\n\n  it(\"preserves URL query parameters\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        url=\"https://example.com/article?utm_source=google&page=1\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.url).toBe(\n      \"https://example.com/article?utm_source=google&page=1\",\n    );\n  });\n\n  it(\"renders NewsArticle type when specified\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        type=\"NewsArticle\"\n        headline=\"Breaking News\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toBe(\"NewsArticle\");\n  });\n\n  it(\"renders BlogPosting type when specified\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        type=\"BlogPosting\"\n        headline=\"My Blog Post\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toBe(\"BlogPosting\");\n  });\n\n  it(\"renders Blog type when specified\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        type=\"Blog\"\n        headline=\"My Blog\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toBe(\"Blog\");\n  });\n\n  it(\"handles string author\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        author=\"John Doe\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n    });\n  });\n\n  it(\"handles Person author object\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        author={{\n          \"@type\": \"Person\",\n          name: \"John Doe\",\n          url: \"https://example.com/john\",\n        }}\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n      url: \"https://example.com/john\",\n    });\n  });\n\n  it(\"handles Organization author\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        author={{\n          \"@type\": \"Organization\",\n          name: \"Example Corp\",\n          url: \"https://example.com\",\n        }}\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Example Corp\",\n      url: \"https://example.com\",\n    });\n  });\n\n  it(\"handles multiple authors\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        author={[\n          \"John Doe\",\n          {\n            \"@type\": \"Person\",\n            name: \"Jane Smith\",\n            url: \"https://example.com/jane\",\n          },\n        ]}\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.author).toEqual([\n      {\n        \"@type\": \"Person\",\n        name: \"John Doe\",\n      },\n      {\n        \"@type\": \"Person\",\n        name: \"Jane Smith\",\n        url: \"https://example.com/jane\",\n      },\n    ]);\n  });\n\n  it(\"handles string image\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        image=\"https://example.com/image.jpg\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.image).toBe(\"https://example.com/image.jpg\");\n  });\n\n  it(\"handles ImageObject\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        image={{\n          \"@type\": \"ImageObject\",\n          url: \"https://example.com/image.jpg\",\n          width: 1200,\n          height: 630,\n        }}\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.image).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/image.jpg\",\n      width: 1200,\n      height: 630,\n    });\n  });\n\n  it(\"handles multiple images\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        image={[\n          \"https://example.com/image1.jpg\",\n          {\n            \"@type\": \"ImageObject\",\n            url: \"https://example.com/image2.jpg\",\n            width: 800,\n            height: 600,\n          },\n        ]}\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.image).toEqual([\n      \"https://example.com/image1.jpg\",\n      {\n        \"@type\": \"ImageObject\",\n        url: \"https://example.com/image2.jpg\",\n        width: 800,\n        height: 600,\n      },\n    ]);\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Full Article\"\n        url=\"https://example.com/article\"\n        author=\"John Doe\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        dateModified=\"2024-01-02T00:00:00.000Z\"\n        image=\"https://example.com/image.jpg\"\n        description=\"This is a full article with all properties\"\n        publisher={{\n          \"@type\": \"Organization\",\n          name: \"Example Publisher\",\n          logo: \"https://example.com/logo.jpg\",\n        }}\n        isAccessibleForFree={true}\n        mainEntityOfPage=\"https://example.com/article\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Article\",\n      headline: \"Full Article\",\n      url: \"https://example.com/article\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"John Doe\",\n      },\n      datePublished: \"2024-01-01T00:00:00.000Z\",\n      dateModified: \"2024-01-02T00:00:00.000Z\",\n      image: \"https://example.com/image.jpg\",\n      description: \"This is a full article with all properties\",\n      publisher: {\n        \"@type\": \"Organization\",\n        name: \"Example Publisher\",\n        logo: \"https://example.com/logo.jpg\",\n      },\n      isAccessibleForFree: true,\n      mainEntityOfPage: \"https://example.com/article\",\n    });\n  });\n\n  it(\"uses datePublished as dateModified when dateModified is not provided\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.dateModified).toBe(\"2024-01-01T00:00:00.000Z\");\n  });\n\n  it(\"handles isAccessibleForFree as false\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Premium Article\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        isAccessibleForFree={false}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.isAccessibleForFree).toBe(false);\n  });\n\n  it(\"uses custom scriptId when provided\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        scriptId=\"custom-article-id\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script!.getAttribute(\"id\")).toBe(\"custom-article-id\");\n    expect(script!.getAttribute(\"data-testid\")).toBe(\"custom-article-id\");\n  });\n\n  it(\"uses custom scriptKey when provided\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        scriptKey=\"custom-key\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    // React doesn't expose the key prop as an attribute, so we can't test it directly\n    // The key is used internally by React for reconciliation\n    expect(script).toBeTruthy();\n  });\n\n  it(\"handles string publisher\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        publisher=\"Example Publisher\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.publisher).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Example Publisher\",\n    });\n  });\n\n  it(\"handles publisher object without @type\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        publisher={{\n          name: \"Example Publisher\",\n          url: \"https://example.com\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.publisher).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Example Publisher\",\n      url: \"https://example.com\",\n    });\n  });\n\n  it(\"handles Person publisher with @type\", () => {\n    const { container } = render(\n      <ArticleJsonLd\n        headline=\"Test Article\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        publisher={{\n          \"@type\": \"Person\",\n          name: \"John Doe\",\n          url: \"https://johndoe.com\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.publisher).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n      url: \"https://johndoe.com\",\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/ArticleJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { ArticleJsonLdProps } from \"~/types/article.types\";\nimport {\n  processAuthor,\n  processImage,\n  processPublisher,\n  processMainEntityOfPage,\n} from \"~/utils/processors\";\n\nexport default function ArticleJsonLd({\n  type = \"Article\",\n  scriptId,\n  scriptKey,\n  headline,\n  url,\n  author,\n  datePublished,\n  dateModified,\n  image,\n  publisher,\n  description,\n  isAccessibleForFree,\n  mainEntityOfPage,\n}: ArticleJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": type,\n    headline,\n    ...(url && { url }),\n    ...(author && {\n      author: Array.isArray(author)\n        ? author.map(processAuthor)\n        : processAuthor(author),\n    }),\n    ...(datePublished && { datePublished }),\n    ...(dateModified && { dateModified }),\n    // If dateModified is not provided but datePublished is, use datePublished\n    ...(!dateModified && datePublished && { dateModified: datePublished }),\n    ...(image && {\n      image: Array.isArray(image)\n        ? image.map(processImage)\n        : processImage(image),\n    }),\n    ...(publisher && { publisher: processPublisher(publisher) }),\n    ...(description && { description }),\n    ...(isAccessibleForFree !== undefined && { isAccessibleForFree }),\n    ...(mainEntityOfPage && {\n      mainEntityOfPage: processMainEntityOfPage(mainEntityOfPage),\n    }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || `article-jsonld-${type}`}\n    />\n  );\n}\n\nexport type { ArticleJsonLdProps };\n"
  },
  {
    "path": "src/components/BreadcrumbJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport BreadcrumbJsonLd from \"./BreadcrumbJsonLd\";\n\ndescribe(\"BreadcrumbJsonLd\", () => {\n  it(\"renders basic breadcrumb trail with minimal props\", () => {\n    const { container } = render(\n      <BreadcrumbJsonLd\n        items={[\n          { name: \"Books\", item: \"https://example.com/books\" },\n          { name: \"Science Fiction\", item: \"https://example.com/books/sci-fi\" },\n          { name: \"Award Winners\" },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"BreadcrumbList\",\n      itemListElement: [\n        {\n          \"@type\": \"ListItem\",\n          position: 1,\n          name: \"Books\",\n          item: \"https://example.com/books\",\n        },\n        {\n          \"@type\": \"ListItem\",\n          position: 2,\n          name: \"Science Fiction\",\n          item: \"https://example.com/books/sci-fi\",\n        },\n        {\n          \"@type\": \"ListItem\",\n          position: 3,\n          name: \"Award Winners\",\n        },\n      ],\n    });\n  });\n\n  it(\"renders multiple breadcrumb trails\", () => {\n    const { container } = render(\n      <BreadcrumbJsonLd\n        multipleTrails={[\n          [\n            { name: \"Books\", item: \"https://example.com/books\" },\n            {\n              name: \"Science Fiction\",\n              item: \"https://example.com/books/sci-fi\",\n            },\n            { name: \"Award Winners\" },\n          ],\n          [\n            { name: \"Literature\", item: \"https://example.com/literature\" },\n            { name: \"Award Winners\" },\n          ],\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual([\n      {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"BreadcrumbList\",\n        itemListElement: [\n          {\n            \"@type\": \"ListItem\",\n            position: 1,\n            name: \"Books\",\n            item: \"https://example.com/books\",\n          },\n          {\n            \"@type\": \"ListItem\",\n            position: 2,\n            name: \"Science Fiction\",\n            item: \"https://example.com/books/sci-fi\",\n          },\n          {\n            \"@type\": \"ListItem\",\n            position: 3,\n            name: \"Award Winners\",\n          },\n        ],\n      },\n      {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"BreadcrumbList\",\n        itemListElement: [\n          {\n            \"@type\": \"ListItem\",\n            position: 1,\n            name: \"Literature\",\n            item: \"https://example.com/literature\",\n          },\n          {\n            \"@type\": \"ListItem\",\n            position: 2,\n            name: \"Award Winners\",\n          },\n        ],\n      },\n    ]);\n  });\n\n  it(\"handles Thing objects with @id\", () => {\n    const { container } = render(\n      <BreadcrumbJsonLd\n        items={[\n          { name: \"Books\", item: { \"@id\": \"https://example.com/books\" } },\n          { name: \"Authors\", item: { \"@id\": \"https://example.com/authors\" } },\n          { name: \"Ann Leckie\" },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.itemListElement[0].item).toEqual({\n      \"@id\": \"https://example.com/books\",\n    });\n    expect(jsonData.itemListElement[1].item).toEqual({\n      \"@id\": \"https://example.com/authors\",\n    });\n  });\n\n  it(\"renders with custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <BreadcrumbJsonLd\n        items={[\n          { name: \"Home\", item: \"https://example.com\" },\n          { name: \"Products\" },\n        ]}\n        scriptId=\"custom-breadcrumb\"\n        scriptKey=\"my-breadcrumb-key\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n    expect(script!.getAttribute(\"id\")).toBe(\"custom-breadcrumb\");\n    expect(script!.getAttribute(\"data-testid\")).toBe(\"custom-breadcrumb\");\n  });\n\n  it(\"handles single item breadcrumb\", () => {\n    const { container } = render(\n      <BreadcrumbJsonLd items={[{ name: \"Home\" }]} />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.itemListElement).toHaveLength(1);\n    expect(jsonData.itemListElement[0]).toEqual({\n      \"@type\": \"ListItem\",\n      position: 1,\n      name: \"Home\",\n    });\n  });\n\n  it(\"handles empty breadcrumb trail\", () => {\n    const { container } = render(<BreadcrumbJsonLd items={[]} />);\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.itemListElement).toEqual([]);\n  });\n\n  it(\"preserves URL query parameters\", () => {\n    const { container } = render(\n      <BreadcrumbJsonLd\n        items={[\n          {\n            name: \"Search\",\n            item: \"https://example.com/search?q=books&sort=date\",\n          },\n          { name: \"Results\" },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.itemListElement[0].item).toBe(\n      \"https://example.com/search?q=books&sort=date\",\n    );\n  });\n\n  it(\"handles multiple trails with custom scriptKey\", () => {\n    const { container } = render(\n      <BreadcrumbJsonLd\n        multipleTrails={[[{ name: \"Path 1\" }], [{ name: \"Path 2\" }]]}\n        scriptId=\"custom-multiple-id\"\n        scriptKey=\"custom-multiple-key\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script!.getAttribute(\"id\")).toBe(\"custom-multiple-id\");\n    expect(script!.getAttribute(\"data-testid\")).toBe(\"custom-multiple-id\");\n  });\n});\n"
  },
  {
    "path": "src/components/BreadcrumbJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { BreadcrumbJsonLdProps } from \"~/types/breadcrumb.types\";\nimport { processBreadcrumbItem } from \"~/utils/processors\";\n\nexport default function BreadcrumbJsonLd(props: BreadcrumbJsonLdProps) {\n  const { scriptId, scriptKey } = props;\n\n  // Handle single trail vs multiple trails\n  const hasMultipleTrails = \"multipleTrails\" in props;\n\n  if (hasMultipleTrails) {\n    // Multiple breadcrumb trails\n    const data = props.multipleTrails.map((trail) => ({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"BreadcrumbList\",\n      itemListElement: trail.map((item, index) =>\n        processBreadcrumbItem(item, index + 1),\n      ),\n    }));\n\n    return (\n      <JsonLdScript\n        data={data}\n        id={scriptId}\n        scriptKey={scriptKey || \"breadcrumb-jsonld-multiple\"}\n      />\n    );\n  } else {\n    // Single breadcrumb trail\n    const data = {\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"BreadcrumbList\",\n      itemListElement: props.items.map((item, index) =>\n        processBreadcrumbItem(item, index + 1),\n      ),\n    };\n\n    return (\n      <JsonLdScript\n        data={data}\n        id={scriptId}\n        scriptKey={scriptKey || \"breadcrumb-jsonld\"}\n      />\n    );\n  }\n}\n\nexport type { BreadcrumbJsonLdProps };\n"
  },
  {
    "path": "src/components/CLAUDE.md",
    "content": "# Component Implementation Guidelines\n\n## Critical Pattern: @type Optional\n\n**NEVER require developers to specify `@type` properties!** This is core to next-seo's design.\n\n### Implementation Rules\n\n1. Component props use `Omit<Type, \"@type\">` to make `@type` optional\n2. Process functions automatically add correct `@type` based on input\n3. Accept flexible inputs: strings, objects with/without `@type`, arrays\n\n### Example\n\n```typescript\n// Props definition\nexport type ArticleJsonLdProps = (\n  | Omit<Article, \"@type\">\n  | Omit<NewsArticle, \"@type\">\n) & {\n  type?: \"Article\" | \"NewsArticle\";\n  // ...\n};\n\n// Component sets @type from type prop\nconst data = {\n  \"@context\": \"https://schema.org\",\n  \"@type\": type, // Set from prop, not user input\n  // ...\n};\n```\n\n## Component Structure\n\n```typescript\n// [Component]JsonLd.tsx\nimport { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { [Component]JsonLdProps } from \"~/types/[component].types\";\nimport { processAuthor, processImage } from \"~/utils/processors\";\n\nexport default function [Component]JsonLd({\n  type = \"[DefaultType]\",\n  scriptId,\n  scriptKey,\n  // ... props\n}: [Component]JsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": type,\n    // Use conditional spread for optional props\n    ...(url && { url }),\n    // Use process functions for flexible inputs\n    ...(author && {\n      author: Array.isArray(author)\n        ? author.map(processAuthor)\n        : processAuthor(author),\n    }),\n    // Boolean checks need explicit undefined check\n    ...(isAccessibleForFree !== undefined && { isAccessibleForFree }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || `[component]-jsonld-${type}`}\n    />\n  );\n}\n\nexport type { [Component]JsonLdProps };\n```\n\n## Process Functions\n\n**ALWAYS use process functions** from `~/utils/processors` for flexible properties:\n\n```typescript\n// Converts strings to objects with @type\nprocessAuthor(\"John Doe\"); // → { \"@type\": \"Person\", name: \"John Doe\" }\nprocessImage(\"url.jpg\"); // → \"url.jpg\" (returns as-is if string)\n\n// Adds @type to objects intelligently\nprocessAuthor({ name: \"ACME\", logo: \"...\" }); // → Organization\nprocessAuthor({ name: \"John\" }); // → Person\n```\n\n## Unit Testing\n\nTests are **co-located** with components as `[Component]JsonLd.test.tsx`\n\n### Test Structure\n\n```typescript\nimport { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport [Component]JsonLd from \"./[Component]JsonLd\";\n\ndescribe(\"[Component]JsonLd\", () => {\n  it(\"renders basic [Component] with minimal props\", () => {\n    const { container } = render(\n      <[Component]JsonLd\n        // Minimal required props\n      />\n    );\n\n    const script = container.querySelector('script[type=\"application/ld+json\"]');\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"[Type]\");\n  });\n});\n```\n\n### Test Coverage Checklist\n\n- ✅ Basic rendering with minimal props\n- ✅ All schema type variations\n- ✅ String to object conversions\n- ✅ Array handling\n- ✅ All optional properties\n- ✅ Default value application\n- ✅ Boolean value handling (false values)\n- ✅ Custom scriptId and scriptKey\n\n## Commands\n\n```bash\npnpm test:unit       # Run all unit tests\npnpm test:unit:watch # Watch mode\npnpm typecheck      # Type checking\npnpm lint           # ESLint\n```\n\n## Important Patterns\n\n### Conditional Property Inclusion\n\n```typescript\n// Only include if truthy\n...(url && { url }),\n\n// Include boolean false values\n...(isAccessibleForFree !== undefined && { isAccessibleForFree }),\n\n// Apply defaults\n...(!dateModified && datePublished && { dateModified: datePublished }),\n```\n\n### Shared Utilities\n\n- Types: Import from `~/types/common.types`\n- Processors: Import from `~/utils/processors`\n- Never duplicate process functions or types\n\n## Checklist for New Components\n\n1. Research Google's structured data docs\n2. Create types in `~/types/[component].types.ts`\n3. Implement component using process functions\n4. Export from `src/index.ts`\n5. Write comprehensive unit tests\n6. Create example pages\n7. Add E2E tests\n"
  },
  {
    "path": "src/components/CarouselJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport CarouselJsonLd from \"./CarouselJsonLd\";\n\ndescribe(\"CarouselJsonLd\", () => {\n  // Summary Page Pattern Tests\n  describe(\"Summary Page Pattern\", () => {\n    it(\"renders ItemList with URLs only (string array)\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          urls={[\n            \"https://example.com/page1\",\n            \"https://example.com/page2\",\n            \"https://example.com/page3\",\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script).toBeTruthy();\n\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData).toEqual({\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"ItemList\",\n        itemListElement: [\n          {\n            \"@type\": \"ListItem\",\n            position: 1,\n            url: \"https://example.com/page1\",\n          },\n          {\n            \"@type\": \"ListItem\",\n            position: 2,\n            url: \"https://example.com/page2\",\n          },\n          {\n            \"@type\": \"ListItem\",\n            position: 3,\n            url: \"https://example.com/page3\",\n          },\n        ],\n      });\n    });\n\n    it(\"renders ItemList with URLs and custom positions\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          urls={[\n            { url: \"https://example.com/page1\", position: 5 },\n            \"https://example.com/page2\",\n            { url: \"https://example.com/page3\", position: 10 },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script).toBeTruthy();\n\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData.itemListElement[0].position).toBe(5);\n      expect(jsonData.itemListElement[1].position).toBe(2);\n      expect(jsonData.itemListElement[2].position).toBe(10);\n    });\n\n    it(\"uses custom scriptId and scriptKey\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          urls={[\"https://example.com/page1\"]}\n          scriptId=\"custom-id\"\n          scriptKey=\"custom-key\"\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script?.getAttribute(\"id\")).toBe(\"custom-id\");\n      expect(script?.getAttribute(\"data-testid\")).toBe(\"custom-id\");\n    });\n  });\n\n  // Course Carousel Tests\n  describe(\"Course Carousel\", () => {\n    it(\"renders Course carousel with minimal props\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          contentType=\"Course\"\n          items={[\n            {\n              name: \"Introduction to React\",\n              description: \"Learn the basics of React\",\n            },\n            {\n              name: \"Advanced TypeScript\",\n              description: \"Master TypeScript features\",\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script).toBeTruthy();\n\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData[\"@type\"]).toBe(\"ItemList\");\n      expect(jsonData.itemListElement).toHaveLength(2);\n      expect(jsonData.itemListElement[0]).toEqual({\n        \"@type\": \"ListItem\",\n        position: 1,\n        item: {\n          \"@type\": \"Course\",\n          name: \"Introduction to React\",\n          description: \"Learn the basics of React\",\n        },\n      });\n    });\n\n    it(\"renders Course carousel with provider\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          contentType=\"Course\"\n          items={[\n            {\n              name: \"Web Development\",\n              description: \"Full stack web development\",\n              url: \"https://example.com/course/web-dev\",\n              provider: \"Tech Academy\",\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      expect(jsonData.itemListElement[0].item.provider).toEqual({\n        \"@type\": \"Organization\",\n        name: \"Tech Academy\",\n      });\n    });\n  });\n\n  // Movie Carousel Tests\n  describe(\"Movie Carousel\", () => {\n    it(\"renders Movie carousel with minimal props\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          contentType=\"Movie\"\n          items={[\n            {\n              name: \"The Matrix\",\n              image: \"https://example.com/matrix.jpg\",\n            },\n            {\n              name: \"Inception\",\n              image: [\n                \"https://example.com/inception1.jpg\",\n                \"https://example.com/inception2.jpg\",\n              ],\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script).toBeTruthy();\n\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData.itemListElement).toHaveLength(2);\n      expect(jsonData.itemListElement[0].item[\"@type\"]).toBe(\"Movie\");\n      expect(jsonData.itemListElement[0].item.name).toBe(\"The Matrix\");\n      expect(jsonData.itemListElement[0].item.image).toBe(\n        \"https://example.com/matrix.jpg\",\n      );\n    });\n\n    it(\"renders Movie carousel with director and reviews\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          contentType=\"Movie\"\n          items={[\n            {\n              name: \"The Dark Knight\",\n              image: \"https://example.com/dark-knight.jpg\",\n              director: \"Christopher Nolan\",\n              review: {\n                reviewRating: {\n                  ratingValue: 5,\n                },\n                author: \"Movie Critic\",\n              },\n              aggregateRating: {\n                ratingValue: 9.0,\n                ratingCount: 1000,\n              },\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      const movie = jsonData.itemListElement[0].item;\n      expect(movie.director).toEqual({\n        \"@type\": \"Person\",\n        name: \"Christopher Nolan\",\n      });\n      expect(movie.review[\"@type\"]).toBe(\"Review\");\n      expect(movie.aggregateRating[\"@type\"]).toBe(\"AggregateRating\");\n    });\n  });\n\n  // Recipe Carousel Tests\n  describe(\"Recipe Carousel\", () => {\n    it(\"renders Recipe carousel with minimal props\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          contentType=\"Recipe\"\n          items={[\n            {\n              name: \"Chocolate Chip Cookies\",\n              image: \"https://example.com/cookies.jpg\",\n            },\n            {\n              name: \"Banana Bread\",\n              image: {\n                url: \"https://example.com/banana-bread.jpg\",\n                width: 800,\n                height: 600,\n              },\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script).toBeTruthy();\n\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData.itemListElement).toHaveLength(2);\n      expect(jsonData.itemListElement[0].item[\"@type\"]).toBe(\"Recipe\");\n      expect(jsonData.itemListElement[0].item.name).toBe(\n        \"Chocolate Chip Cookies\",\n      );\n    });\n\n    it(\"renders Recipe carousel with full details\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          contentType=\"Recipe\"\n          items={[\n            {\n              name: \"Perfect Pancakes\",\n              image: \"https://example.com/pancakes.jpg\",\n              description: \"Fluffy and delicious pancakes\",\n              author: \"Chef John\",\n              datePublished: \"2024-01-01\",\n              prepTime: \"PT10M\",\n              cookTime: \"PT15M\",\n              totalTime: \"PT25M\",\n              recipeYield: 4,\n              recipeCategory: \"Breakfast\",\n              recipeCuisine: \"American\",\n              recipeIngredient: [\"2 cups flour\", \"2 eggs\", \"1 cup milk\"],\n              recipeInstructions: [\n                \"Mix dry ingredients\",\n                \"Add wet ingredients\",\n                \"Cook on griddle\",\n              ],\n              nutrition: {\n                calories: \"250 calories\",\n                proteinContent: \"8g\",\n              },\n              aggregateRating: {\n                ratingValue: 4.5,\n                ratingCount: 100,\n              },\n              keywords: \"pancakes, breakfast, easy\",\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      const recipe = jsonData.itemListElement[0].item;\n      expect(recipe.author).toEqual({\n        \"@type\": \"Person\",\n        name: \"Chef John\",\n      });\n      expect(recipe.nutrition[\"@type\"]).toBe(\"NutritionInformation\");\n      expect(recipe.recipeInstructions).toEqual([\n        \"Mix dry ingredients\",\n        \"Add wet ingredients\",\n        \"Cook on griddle\",\n      ]);\n    });\n\n    it(\"handles complex recipe instructions\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          contentType=\"Recipe\"\n          items={[\n            {\n              name: \"Complex Recipe\",\n              image: \"https://example.com/recipe.jpg\",\n              recipeInstructions: [\n                { text: \"Step 1: Prepare ingredients\" },\n                {\n                  name: \"Cooking Phase\",\n                  itemListElement: [\n                    { text: \"Heat the oven\" },\n                    { text: \"Mix ingredients\" },\n                  ],\n                },\n              ],\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      const instructions = jsonData.itemListElement[0].item.recipeInstructions;\n      expect(instructions[0][\"@type\"]).toBe(\"HowToStep\");\n      expect(instructions[1][\"@type\"]).toBe(\"HowToSection\");\n      expect(instructions[1].itemListElement[0][\"@type\"]).toBe(\"HowToStep\");\n    });\n  });\n\n  // Restaurant Carousel Tests\n  describe(\"Restaurant Carousel\", () => {\n    it(\"renders Restaurant carousel with minimal props\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          contentType=\"Restaurant\"\n          items={[\n            {\n              name: \"Joe's Pizza\",\n              address: \"123 Pizza St\",\n            },\n            {\n              name: \"Thai Palace\",\n              address: \"456 Thai Ave\",\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script).toBeTruthy();\n\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData.itemListElement).toHaveLength(2);\n      expect(jsonData.itemListElement[0].item[\"@type\"]).toBe(\"Restaurant\");\n      expect(jsonData.itemListElement[0].item.name).toBe(\"Joe's Pizza\");\n    });\n\n    it(\"renders Restaurant carousel with full details\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          contentType=\"Restaurant\"\n          items={[\n            {\n              name: \"Fine Dining Restaurant\",\n              address: \"123 Main St, New York, NY 10001\",\n              telephone: \"+1-212-555-0100\",\n              url: \"https://example.com/restaurant\",\n              image: \"https://example.com/restaurant.jpg\",\n              priceRange: \"$$$\",\n              servesCuisine: [\"Italian\", \"Mediterranean\"],\n              menu: \"https://example.com/menu\",\n              geo: {\n                latitude: 40.7128,\n                longitude: -74.006,\n              },\n              openingHoursSpecification: [\n                {\n                  dayOfWeek: \"Monday\",\n                  opens: \"11:00\",\n                  closes: \"22:00\",\n                },\n              ],\n              aggregateRating: {\n                ratingValue: 4.7,\n                ratingCount: 250,\n              },\n              review: {\n                reviewRating: { ratingValue: 5 },\n                author: \"Food Critic\",\n                reviewBody: \"Excellent food and service\",\n              },\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      const restaurant = jsonData.itemListElement[0].item;\n      expect(restaurant.address).toBe(\"123 Main St, New York, NY 10001\");\n      expect(restaurant.geo[\"@type\"]).toBe(\"GeoCoordinates\");\n      expect(restaurant.openingHoursSpecification[0][\"@type\"]).toBe(\n        \"OpeningHoursSpecification\",\n      );\n      expect(restaurant.review[\"@type\"]).toBe(\"Review\");\n    });\n\n    it(\"handles complex address formats\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          contentType=\"Restaurant\"\n          items={[\n            {\n              name: \"International Restaurant\",\n              address: {\n                streetAddress: \"456 Oak Avenue\",\n                addressLocality: \"San Francisco\",\n                addressRegion: \"CA\",\n                postalCode: \"94102\",\n                addressCountry: \"US\",\n              },\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      const address = jsonData.itemListElement[0].item.address;\n      expect(address[\"@type\"]).toBe(\"PostalAddress\");\n      expect(address.streetAddress).toBe(\"456 Oak Avenue\");\n      expect(address.addressLocality).toBe(\"San Francisco\");\n    });\n  });\n\n  // Edge Cases and Error Handling\n  describe(\"Edge Cases\", () => {\n    it(\"handles empty URLs array\", () => {\n      const { container } = render(<CarouselJsonLd urls={[]} />);\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      expect(jsonData.itemListElement).toEqual([]);\n    });\n\n    it(\"handles empty items array\", () => {\n      const { container } = render(\n        <CarouselJsonLd contentType=\"Course\" items={[]} />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      expect(jsonData.itemListElement).toEqual([]);\n    });\n\n    it(\"handles mixed image formats in Recipe\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          contentType=\"Recipe\"\n          items={[\n            {\n              name: \"Multi-Image Recipe\",\n              image: [\n                \"https://example.com/image1.jpg\",\n                {\n                  url: \"https://example.com/image2.jpg\",\n                  width: 800,\n                  height: 600,\n                },\n                \"https://example.com/image3.jpg\",\n              ],\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      const images = jsonData.itemListElement[0].item.image;\n      expect(images).toHaveLength(3);\n      expect(images[0]).toBe(\"https://example.com/image1.jpg\");\n      expect(images[1][\"@type\"]).toBe(\"ImageObject\");\n    });\n\n    it(\"handles simple author in Recipe\", () => {\n      const { container } = render(\n        <CarouselJsonLd\n          contentType=\"Recipe\"\n          items={[\n            {\n              name: \"Collaborative Recipe\",\n              image: \"https://example.com/recipe.jpg\",\n              author: \"Chef Alice\",\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      const author = jsonData.itemListElement[0].item.author;\n      expect(author).toEqual({\n        \"@type\": \"Person\",\n        name: \"Chef Alice\",\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/CarouselJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type {\n  CarouselJsonLdProps,\n  CarouselListItem,\n  SummaryPageItem,\n  CourseItem,\n  MovieItem,\n  RecipeItem,\n  RestaurantItem,\n} from \"~/types/carousel.types\";\nimport type { Course } from \"~/types/course.types\";\nimport type { Movie } from \"~/types/movie-carousel.types\";\nimport type { Recipe } from \"~/types/recipe.types\";\nimport type { Restaurant } from \"~/types/localbusiness.types\";\nimport {\n  processProvider,\n  processImage,\n  processDirector,\n  processReview,\n  processAggregateRating,\n  processVideo,\n  processNutrition,\n  processInstruction,\n  processAddress,\n  processGeo,\n  processOpeningHours,\n} from \"~/utils/processors\";\n\nfunction processSummaryItem(\n  item: SummaryPageItem,\n  index: number,\n): CarouselListItem {\n  if (typeof item === \"string\") {\n    return {\n      \"@type\": \"ListItem\",\n      position: index + 1,\n      url: item,\n    };\n  }\n  return {\n    \"@type\": \"ListItem\",\n    position: item.position ?? index + 1,\n    url: item.url,\n  };\n}\n\nfunction processCourseItem(\n  course: CourseItem,\n  index: number,\n): CarouselListItem {\n  const processedCourse: Course = {\n    \"@type\": \"Course\",\n    name: course.name,\n    description: course.description,\n    ...(course.url && { url: course.url }),\n    ...(course.provider && { provider: processProvider(course.provider) }),\n  };\n\n  return {\n    \"@type\": \"ListItem\",\n    position: index + 1,\n    item: processedCourse,\n  };\n}\n\nfunction processMovieItem(movie: MovieItem, index: number): CarouselListItem {\n  const processedMovie: Movie = {\n    \"@type\": \"Movie\",\n    name: movie.name,\n    image: Array.isArray(movie.image)\n      ? movie.image.map(processImage)\n      : processImage(movie.image),\n    ...(movie.url && { url: movie.url }),\n    ...(movie.dateCreated && { dateCreated: movie.dateCreated }),\n    ...(movie.director && { director: processDirector(movie.director) }),\n    ...(movie.review && { review: processReview(movie.review) }),\n    ...(movie.aggregateRating && {\n      aggregateRating: processAggregateRating(movie.aggregateRating),\n    }),\n  };\n\n  return {\n    \"@type\": \"ListItem\",\n    position: index + 1,\n    item: processedMovie,\n  };\n}\n\nfunction processRecipeItem(\n  recipe: RecipeItem,\n  index: number,\n): CarouselListItem {\n  const processedRecipe: Recipe = {\n    \"@type\": \"Recipe\",\n    name: recipe.name,\n    image: Array.isArray(recipe.image)\n      ? recipe.image.map(processImage)\n      : processImage(recipe.image),\n    ...(recipe.description && { description: recipe.description }),\n    ...(recipe.author && {\n      author:\n        typeof recipe.author === \"string\"\n          ? { \"@type\": \"Person\", name: recipe.author }\n          : recipe.author,\n    }),\n    ...(recipe.datePublished && { datePublished: recipe.datePublished }),\n    ...(recipe.prepTime && { prepTime: recipe.prepTime }),\n    ...(recipe.cookTime && { cookTime: recipe.cookTime }),\n    ...(recipe.totalTime && { totalTime: recipe.totalTime }),\n    ...(recipe.recipeYield && { recipeYield: recipe.recipeYield }),\n    ...(recipe.recipeCategory && { recipeCategory: recipe.recipeCategory }),\n    ...(recipe.recipeCuisine && { recipeCuisine: recipe.recipeCuisine }),\n    ...(recipe.nutrition && { nutrition: processNutrition(recipe.nutrition) }),\n    ...(recipe.recipeIngredient && {\n      recipeIngredient: recipe.recipeIngredient,\n    }),\n    ...(recipe.recipeInstructions && {\n      recipeInstructions: Array.isArray(recipe.recipeInstructions)\n        ? recipe.recipeInstructions.map(processInstruction)\n        : processInstruction(recipe.recipeInstructions),\n    }),\n    ...(recipe.aggregateRating && {\n      aggregateRating: processAggregateRating(recipe.aggregateRating),\n    }),\n    ...(recipe.video && { video: processVideo(recipe.video) }),\n    ...(recipe.keywords && { keywords: recipe.keywords }),\n    ...(recipe.url && { url: recipe.url }),\n  };\n\n  return {\n    \"@type\": \"ListItem\",\n    position: index + 1,\n    item: processedRecipe,\n  };\n}\n\nfunction processRestaurantItem(\n  restaurant: RestaurantItem,\n  index: number,\n): CarouselListItem {\n  const address = restaurant.address\n    ? Array.isArray(restaurant.address)\n      ? restaurant.address.map((addr) =>\n          typeof addr === \"string\" ? addr : processAddress(addr),\n        )\n      : typeof restaurant.address === \"string\"\n        ? restaurant.address\n        : processAddress(restaurant.address)\n    : \"\";\n\n  const processedRestaurant: Restaurant = {\n    \"@type\": \"Restaurant\",\n    name: restaurant.name,\n    address,\n    ...(restaurant.url && { url: restaurant.url }),\n    ...(restaurant.telephone && { telephone: restaurant.telephone }),\n    ...(restaurant.image && {\n      image: Array.isArray(restaurant.image)\n        ? restaurant.image.map(processImage)\n        : processImage(restaurant.image),\n    }),\n    ...(restaurant.priceRange && { priceRange: restaurant.priceRange }),\n    ...(restaurant.geo && { geo: processGeo(restaurant.geo) }),\n    ...(restaurant.openingHoursSpecification && {\n      openingHoursSpecification: Array.isArray(\n        restaurant.openingHoursSpecification,\n      )\n        ? restaurant.openingHoursSpecification.map(processOpeningHours)\n        : processOpeningHours(restaurant.openingHoursSpecification),\n    }),\n    ...(restaurant.review && {\n      review: Array.isArray(restaurant.review)\n        ? restaurant.review.map(processReview)\n        : processReview(restaurant.review),\n    }),\n    ...(restaurant.aggregateRating && {\n      aggregateRating: processAggregateRating(restaurant.aggregateRating),\n    }),\n    ...(restaurant.menu && { menu: restaurant.menu }),\n    ...(restaurant.servesCuisine && {\n      servesCuisine: restaurant.servesCuisine,\n    }),\n    ...(restaurant.sameAs && { sameAs: restaurant.sameAs }),\n  };\n\n  return {\n    \"@type\": \"ListItem\",\n    position: index + 1,\n    item: processedRestaurant,\n  };\n}\n\nexport default function CarouselJsonLd(props: CarouselJsonLdProps) {\n  const { scriptId, scriptKey } = props;\n\n  let itemListElement: CarouselListItem[];\n\n  if (\"urls\" in props) {\n    // Summary page pattern - just URLs\n    itemListElement = props.urls.map((url, index) =>\n      processSummaryItem(url, index),\n    );\n  } else {\n    // All-in-one page pattern - full item data\n    switch (props.contentType) {\n      case \"Course\":\n        itemListElement = props.items.map((item, index) =>\n          processCourseItem(item as CourseItem, index),\n        );\n        break;\n      case \"Movie\":\n        itemListElement = props.items.map((item, index) =>\n          processMovieItem(item as MovieItem, index),\n        );\n        break;\n      case \"Recipe\":\n        itemListElement = props.items.map((item, index) =>\n          processRecipeItem(item as RecipeItem, index),\n        );\n        break;\n      case \"Restaurant\":\n        itemListElement = props.items.map((item, index) =>\n          processRestaurantItem(item as RestaurantItem, index),\n        );\n        break;\n      default:\n        itemListElement = [];\n    }\n  }\n\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"ItemList\",\n    itemListElement,\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"carousel-jsonld\"}\n    />\n  );\n}\n\nexport type { CarouselJsonLdProps };\n"
  },
  {
    "path": "src/components/ClaimReviewJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport ClaimReviewJsonLd from \"./ClaimReviewJsonLd\";\n\ndescribe(\"ClaimReviewJsonLd\", () => {\n  it(\"renders basic ClaimReview with minimal props\", () => {\n    const { container } = render(\n      <ClaimReviewJsonLd\n        claimReviewed=\"The world is flat\"\n        reviewRating={{\n          ratingValue: 1,\n          bestRating: 5,\n          worstRating: 1,\n          alternateName: \"False\",\n        }}\n        url=\"https://example.com/news/science/worldisflat.html\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"ClaimReview\",\n      claimReviewed: \"The world is flat\",\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 1,\n        bestRating: 5,\n        worstRating: 1,\n        alternateName: \"False\",\n      },\n      url: \"https://example.com/news/science/worldisflat.html\",\n    });\n  });\n\n  it(\"handles string author\", () => {\n    const { container } = render(\n      <ClaimReviewJsonLd\n        claimReviewed=\"Test claim\"\n        reviewRating={{\n          ratingValue: 3,\n          alternateName: \"Partially true\",\n        }}\n        url=\"https://example.com/fact-check\"\n        author=\"Example.com science watch\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Example.com science watch\",\n    });\n  });\n\n  it(\"handles organization author with properties\", () => {\n    const { container } = render(\n      <ClaimReviewJsonLd\n        claimReviewed=\"Test claim\"\n        reviewRating={{\n          ratingValue: 3,\n          alternateName: \"Partially true\",\n        }}\n        url=\"https://example.com/fact-check\"\n        author={{\n          name: \"Fact Check Organization\",\n          url: \"https://example.com\",\n          logo: \"https://example.com/logo.jpg\",\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Fact Check Organization\",\n      url: \"https://example.com\",\n      logo: \"https://example.com/logo.jpg\",\n    });\n  });\n\n  it(\"handles review rating with name property\", () => {\n    const { container } = render(\n      <ClaimReviewJsonLd\n        claimReviewed=\"Test claim\"\n        reviewRating={{\n          ratingValue: 2,\n          bestRating: 5,\n          worstRating: 1,\n          alternateName: \"Mostly false\",\n          name: \"Mostly false\",\n        }}\n        url=\"https://example.com/fact-check\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.reviewRating).toEqual({\n      \"@type\": \"Rating\",\n      ratingValue: 2,\n      bestRating: 5,\n      worstRating: 1,\n      alternateName: \"Mostly false\",\n      name: \"Mostly false\",\n    });\n  });\n\n  it(\"handles claim with all properties\", () => {\n    const { container } = render(\n      <ClaimReviewJsonLd\n        claimReviewed=\"The world is flat\"\n        reviewRating={{\n          ratingValue: 1,\n          alternateName: \"False\",\n        }}\n        url=\"https://example.com/fact-check\"\n        itemReviewed={{\n          author: {\n            \"@type\": \"Organization\",\n            name: \"Square World Society\",\n            sameAs:\n              \"https://example.flatworlders.com/we-know-that-the-world-is-flat\",\n          },\n          datePublished: \"2024-06-20\",\n          appearance: {\n            url: \"https://example.com/news/a122121\",\n            headline: \"Square Earth - Flat earthers for the Internet age\",\n            datePublished: \"2024-06-22\",\n            author: \"T. Tellar\",\n            image: \"https://example.com/photos/1x1/photo.jpg\",\n            publisher: {\n              name: \"Skeptical News\",\n              logo: {\n                url: \"https://example.com/logo.jpg\",\n              },\n            },\n          },\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.itemReviewed).toEqual({\n      \"@type\": \"Claim\",\n      author: {\n        \"@type\": \"Organization\",\n        name: \"Square World Society\",\n        sameAs:\n          \"https://example.flatworlders.com/we-know-that-the-world-is-flat\",\n      },\n      datePublished: \"2024-06-20\",\n      appearance: {\n        \"@type\": \"CreativeWork\",\n        url: \"https://example.com/news/a122121\",\n        headline: \"Square Earth - Flat earthers for the Internet age\",\n        datePublished: \"2024-06-22\",\n        author: {\n          \"@type\": \"Person\",\n          name: \"T. Tellar\",\n        },\n        image: \"https://example.com/photos/1x1/photo.jpg\",\n        publisher: {\n          \"@type\": \"Organization\",\n          name: \"Skeptical News\",\n          logo: {\n            \"@type\": \"ImageObject\",\n            url: \"https://example.com/logo.jpg\",\n          },\n        },\n      },\n    });\n  });\n\n  it(\"handles multiple appearances\", () => {\n    const { container } = render(\n      <ClaimReviewJsonLd\n        claimReviewed=\"Test claim\"\n        reviewRating={{\n          ratingValue: 1,\n          alternateName: \"False\",\n        }}\n        url=\"https://example.com/fact-check\"\n        itemReviewed={{\n          appearance: [\n            \"https://example.com/article1\",\n            {\n              url: \"https://example.com/article2\",\n              headline: \"Second appearance\",\n            },\n          ],\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.itemReviewed.appearance).toEqual([\n      \"https://example.com/article1\",\n      {\n        \"@type\": \"CreativeWork\",\n        url: \"https://example.com/article2\",\n        headline: \"Second appearance\",\n      },\n    ]);\n  });\n\n  it(\"handles firstAppearance\", () => {\n    const { container } = render(\n      <ClaimReviewJsonLd\n        claimReviewed=\"Test claim\"\n        reviewRating={{\n          ratingValue: 1,\n          alternateName: \"False\",\n        }}\n        url=\"https://example.com/fact-check\"\n        itemReviewed={{\n          firstAppearance: \"https://example.com/original-article\",\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.itemReviewed.firstAppearance).toBe(\n      \"https://example.com/original-article\",\n    );\n  });\n\n  it(\"handles custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <ClaimReviewJsonLd\n        claimReviewed=\"Test claim\"\n        reviewRating={{\n          ratingValue: 1,\n          alternateName: \"False\",\n        }}\n        url=\"https://example.com/fact-check\"\n        scriptId=\"custom-id\"\n        scriptKey=\"custom-key\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script?.id).toBe(\"custom-id\");\n    expect(script?.getAttribute(\"data-testid\")).toBe(\"custom-id\");\n  });\n\n  it(\"handles ClaimReview without itemReviewed\", () => {\n    const { container } = render(\n      <ClaimReviewJsonLd\n        claimReviewed=\"Politicians claim about economy\"\n        reviewRating={{\n          ratingValue: 2,\n          bestRating: 5,\n          worstRating: 1,\n          alternateName: \"Mostly false\",\n        }}\n        url=\"https://example.com/fact-check/economy\"\n        author={{\n          name: \"Fact Check Team\",\n          url: \"https://example.com\",\n          logo: \"https://example.com/logo.jpg\",\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"ClaimReview\",\n      claimReviewed: \"Politicians claim about economy\",\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 2,\n        bestRating: 5,\n        worstRating: 1,\n        alternateName: \"Mostly false\",\n      },\n      url: \"https://example.com/fact-check/economy\",\n      author: {\n        \"@type\": \"Organization\",\n        name: \"Fact Check Team\",\n        url: \"https://example.com\",\n        logo: \"https://example.com/logo.jpg\",\n      },\n    });\n  });\n\n  it(\"handles claim with string author\", () => {\n    const { container } = render(\n      <ClaimReviewJsonLd\n        claimReviewed=\"Test claim\"\n        reviewRating={{\n          ratingValue: 1,\n          alternateName: \"False\",\n        }}\n        url=\"https://example.com/fact-check\"\n        itemReviewed={{\n          author: \"Original Claimant\",\n          datePublished: \"2024-01-01\",\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.itemReviewed.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Original Claimant\",\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/ClaimReviewJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { ClaimReviewJsonLdProps } from \"~/types/claimreview.types\";\nimport {\n  processAuthor,\n  processClaim,\n  processClaimReviewRating,\n} from \"~/utils/processors\";\n\nexport default function ClaimReviewJsonLd({\n  scriptId,\n  scriptKey,\n  claimReviewed,\n  reviewRating,\n  url,\n  author,\n  itemReviewed,\n}: ClaimReviewJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"ClaimReview\",\n    claimReviewed,\n    reviewRating: processClaimReviewRating(reviewRating),\n    url,\n    ...(author && { author: processAuthor(author) }),\n    ...(itemReviewed && { itemReviewed: processClaim(itemReviewed) }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"claimreview-jsonld\"}\n    />\n  );\n}\n\nexport type { ClaimReviewJsonLdProps };\n"
  },
  {
    "path": "src/components/CourseJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport CourseJsonLd from \"./CourseJsonLd\";\n\ndescribe(\"CourseJsonLd\", () => {\n  // Single course tests\n  describe(\"Single course pattern\", () => {\n    it(\"renders basic Course with minimal props\", () => {\n      const { container } = render(\n        <CourseJsonLd\n          name=\"Introduction to Computer Science\"\n          description=\"An introductory CS course laying out the basics.\"\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script).toBeTruthy();\n\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData).toEqual({\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Course\",\n        name: \"Introduction to Computer Science\",\n        description: \"An introductory CS course laying out the basics.\",\n      });\n    });\n\n    it(\"renders Course with all properties\", () => {\n      const { container } = render(\n        <CourseJsonLd\n          name=\"Advanced Machine Learning\"\n          description=\"Deep dive into modern ML techniques and algorithms.\"\n          url=\"https://example.com/courses/advanced-ml\"\n          provider=\"Tech University\"\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData).toEqual({\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Course\",\n        name: \"Advanced Machine Learning\",\n        description: \"Deep dive into modern ML techniques and algorithms.\",\n        url: \"https://example.com/courses/advanced-ml\",\n        provider: {\n          \"@type\": \"Organization\",\n          name: \"Tech University\",\n        },\n      });\n    });\n\n    it(\"handles provider as Organization object without @type\", () => {\n      const { container } = render(\n        <CourseJsonLd\n          name=\"Web Development Bootcamp\"\n          description=\"Learn full-stack web development.\"\n          provider={{\n            name: \"Code Academy\",\n            url: \"https://codeacademy.com\",\n            sameAs: \"https://twitter.com/codeacademy\",\n          }}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData.provider).toEqual({\n        \"@type\": \"Organization\",\n        name: \"Code Academy\",\n        url: \"https://codeacademy.com\",\n        sameAs: \"https://twitter.com/codeacademy\",\n      });\n    });\n\n    it(\"handles provider as Organization object with @type\", () => {\n      const { container } = render(\n        <CourseJsonLd\n          name=\"Data Science Fundamentals\"\n          description=\"Introduction to data science concepts.\"\n          provider={{\n            \"@type\": \"Organization\",\n            name: \"Data Institute\",\n            logo: \"https://example.com/logo.png\",\n          }}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData.provider).toEqual({\n        \"@type\": \"Organization\",\n        name: \"Data Institute\",\n        logo: \"https://example.com/logo.png\",\n      });\n    });\n\n    it(\"uses custom scriptId and scriptKey\", () => {\n      const { container } = render(\n        <CourseJsonLd\n          name=\"Test Course\"\n          description=\"Test description\"\n          scriptId=\"custom-id\"\n          scriptKey=\"custom-key\"\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script?.getAttribute(\"id\")).toBe(\"custom-id\");\n      expect(script?.getAttribute(\"data-testid\")).toBe(\"custom-id\");\n    });\n  });\n\n  // Course list tests\n  describe(\"Course list pattern\", () => {\n    it(\"renders course list with URLs only (summary page pattern)\", () => {\n      const { container } = render(\n        <CourseJsonLd\n          type=\"list\"\n          urls={[\n            \"https://example.com/courses/intro-cs\",\n            \"https://example.com/courses/intermediate-cs\",\n            \"https://example.com/courses/advanced-cs\",\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData).toEqual({\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"ItemList\",\n        itemListElement: [\n          {\n            \"@type\": \"ListItem\",\n            position: 1,\n            url: \"https://example.com/courses/intro-cs\",\n          },\n          {\n            \"@type\": \"ListItem\",\n            position: 2,\n            url: \"https://example.com/courses/intermediate-cs\",\n          },\n          {\n            \"@type\": \"ListItem\",\n            position: 3,\n            url: \"https://example.com/courses/advanced-cs\",\n          },\n        ],\n      });\n    });\n\n    it(\"renders course list with URL objects and custom positions\", () => {\n      const { container } = render(\n        <CourseJsonLd\n          type=\"list\"\n          urls={[\n            { url: \"https://example.com/courses/python\", position: 2 },\n            { url: \"https://example.com/courses/javascript\", position: 1 },\n            \"https://example.com/courses/golang\", // Will get position 3\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData.itemListElement).toEqual([\n        {\n          \"@type\": \"ListItem\",\n          position: 2,\n          url: \"https://example.com/courses/python\",\n        },\n        {\n          \"@type\": \"ListItem\",\n          position: 1,\n          url: \"https://example.com/courses/javascript\",\n        },\n        {\n          \"@type\": \"ListItem\",\n          position: 3,\n          url: \"https://example.com/courses/golang\",\n        },\n      ]);\n    });\n\n    it(\"renders course list with full course data (all-in-one pattern)\", () => {\n      const { container } = render(\n        <CourseJsonLd\n          type=\"list\"\n          courses={[\n            {\n              name: \"Introduction to Programming\",\n              description: \"Learn the basics of programming.\",\n              url: \"https://example.com/courses/intro-programming\",\n              provider: \"Tech Institute\",\n            },\n            {\n              name: \"Advanced Algorithms\",\n              description: \"Study complex algorithmic solutions.\",\n              provider: {\n                name: \"University Online\",\n                sameAs: \"https://university.edu\",\n              },\n            },\n            {\n              name: \"Web Design Basics\",\n              description: \"Create beautiful and functional websites.\",\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData).toEqual({\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"ItemList\",\n        itemListElement: [\n          {\n            \"@type\": \"ListItem\",\n            position: 1,\n            item: {\n              \"@type\": \"Course\",\n              name: \"Introduction to Programming\",\n              description: \"Learn the basics of programming.\",\n              url: \"https://example.com/courses/intro-programming\",\n              provider: {\n                \"@type\": \"Organization\",\n                name: \"Tech Institute\",\n              },\n            },\n          },\n          {\n            \"@type\": \"ListItem\",\n            position: 2,\n            item: {\n              \"@type\": \"Course\",\n              name: \"Advanced Algorithms\",\n              description: \"Study complex algorithmic solutions.\",\n              provider: {\n                \"@type\": \"Organization\",\n                name: \"University Online\",\n                sameAs: \"https://university.edu\",\n              },\n            },\n          },\n          {\n            \"@type\": \"ListItem\",\n            position: 3,\n            item: {\n              \"@type\": \"Course\",\n              name: \"Web Design Basics\",\n              description: \"Create beautiful and functional websites.\",\n            },\n          },\n        ],\n      });\n    });\n\n    it(\"uses custom scriptId and scriptKey for list\", () => {\n      const { container } = render(\n        <CourseJsonLd\n          type=\"list\"\n          urls={[\"https://example.com/course1\"]}\n          scriptId=\"list-id\"\n          scriptKey=\"list-key\"\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script?.getAttribute(\"id\")).toBe(\"list-id\");\n      expect(script?.getAttribute(\"data-testid\")).toBe(\"list-id\");\n    });\n  });\n\n  // Edge cases\n  describe(\"Edge cases\", () => {\n    it(\"renders single course when type is explicitly set to single\", () => {\n      const { container } = render(\n        <CourseJsonLd\n          type=\"single\"\n          name=\"Explicit Single Course\"\n          description=\"Testing explicit single type.\"\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData[\"@type\"]).toBe(\"Course\");\n      expect(jsonData.name).toBe(\"Explicit Single Course\");\n    });\n\n    it(\"handles empty course list\", () => {\n      const { container } = render(<CourseJsonLd type=\"list\" urls={[]} />);\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData).toEqual({\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"ItemList\",\n        itemListElement: [],\n      });\n    });\n\n    it(\"preserves URL query parameters in course lists\", () => {\n      const { container } = render(\n        <CourseJsonLd\n          type=\"list\"\n          urls={[\"https://example.com/course?utm_source=google&ref=home\"]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData.itemListElement[0].url).toBe(\n        \"https://example.com/course?utm_source=google&ref=home\",\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/CourseJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type {\n  CourseJsonLdProps,\n  CourseListItem,\n  SummaryPageItem,\n  CourseListItemSchema,\n  Course,\n} from \"~/types/course.types\";\nimport { processProvider } from \"~/utils/processors\";\n\nfunction processSummaryItem(\n  item: SummaryPageItem,\n  index: number,\n): CourseListItemSchema {\n  if (typeof item === \"string\") {\n    return {\n      \"@type\": \"ListItem\",\n      position: index + 1,\n      url: item,\n    };\n  }\n  return {\n    \"@type\": \"ListItem\",\n    position: item.position ?? index + 1,\n    url: item.url,\n  };\n}\n\nfunction processCourseItem(\n  course: CourseListItem,\n  index: number,\n): CourseListItemSchema {\n  const processedCourse: Course = {\n    \"@type\": \"Course\",\n    name: course.name,\n    description: course.description,\n    ...(course.url && { url: course.url }),\n    ...(course.provider && { provider: processProvider(course.provider) }),\n  };\n\n  return {\n    \"@type\": \"ListItem\",\n    position: index + 1,\n    item: processedCourse,\n  };\n}\n\nexport default function CourseJsonLd(props: CourseJsonLdProps) {\n  const { scriptId, scriptKey } = props;\n\n  // Single course pattern\n  if (!(\"type\" in props) || props.type === \"single\") {\n    const data = {\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Course\",\n      name: props.name,\n      description: props.description,\n      ...(props.url && { url: props.url }),\n      ...(props.provider && { provider: processProvider(props.provider) }),\n    };\n\n    return (\n      <JsonLdScript\n        data={data}\n        id={scriptId}\n        scriptKey={scriptKey || \"course-jsonld\"}\n      />\n    );\n  }\n\n  // List pattern\n  let itemListElement: CourseListItemSchema[];\n\n  if (\"urls\" in props && props.type === \"list\") {\n    // Summary page pattern - just URLs\n    itemListElement = props.urls.map((url, index) =>\n      processSummaryItem(url, index),\n    );\n  } else if (\"courses\" in props && props.type === \"list\") {\n    // All-in-one page pattern - full course data\n    itemListElement = props.courses.map((course, index) =>\n      processCourseItem(course, index),\n    );\n  } else {\n    // This should never happen due to type constraints\n    itemListElement = [];\n  }\n\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"ItemList\",\n    itemListElement,\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"course-list-jsonld\"}\n    />\n  );\n}\n\nexport type { CourseJsonLdProps };\n"
  },
  {
    "path": "src/components/CreativeWorkJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport CreativeWorkJsonLd from \"./CreativeWorkJsonLd\";\n\ndescribe(\"CreativeWorkJsonLd\", () => {\n  it(\"renders basic CreativeWork with minimal props\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        name=\"Test Creative Work\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"CreativeWork\",\n      name: \"Test Creative Work\",\n      datePublished: \"2024-01-01T00:00:00.000Z\",\n    });\n  });\n\n  it(\"renders Article with paywalled content\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"Article\"\n        headline=\"Paywalled Article\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        isAccessibleForFree={false}\n        hasPart={{\n          isAccessibleForFree: false,\n          cssSelector: \".paywall\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Article\",\n      headline: \"Paywalled Article\",\n      datePublished: \"2024-01-01T00:00:00.000Z\",\n      dateModified: \"2024-01-01T00:00:00.000Z\",\n      isAccessibleForFree: false,\n      hasPart: {\n        \"@type\": \"WebPageElement\",\n        isAccessibleForFree: false,\n        cssSelector: \".paywall\",\n      },\n    });\n  });\n\n  it(\"renders NewsArticle with multiple paywalled sections\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"NewsArticle\"\n        headline=\"Breaking News\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        isAccessibleForFree={false}\n        hasPart={[\n          {\n            isAccessibleForFree: false,\n            cssSelector: \".section1\",\n          },\n          {\n            isAccessibleForFree: false,\n            cssSelector: \".section2\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.hasPart).toEqual([\n      {\n        \"@type\": \"WebPageElement\",\n        isAccessibleForFree: false,\n        cssSelector: \".section1\",\n      },\n      {\n        \"@type\": \"WebPageElement\",\n        isAccessibleForFree: false,\n        cssSelector: \".section2\",\n      },\n    ]);\n  });\n\n  it(\"renders Blog type with subscription content\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"Blog\"\n        name=\"My Premium Blog\"\n        description=\"A blog with premium content\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        isAccessibleForFree={false}\n        hasPart={{\n          isAccessibleForFree: false,\n          cssSelector: \".premium-content\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Blog\");\n    expect(jsonData.name).toBe(\"My Premium Blog\");\n    expect(jsonData.isAccessibleForFree).toBe(false);\n  });\n\n  it(\"renders BlogPosting type\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"BlogPosting\"\n        headline=\"My Blog Post\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toBe(\"BlogPosting\");\n  });\n\n  it(\"renders Comment type with text\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"Comment\"\n        text=\"This is a great article!\"\n        author=\"John Doe\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Comment\");\n    expect(jsonData.text).toBe(\"This is a great article!\");\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n    });\n  });\n\n  it(\"renders Course type with provider\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"Course\"\n        name=\"Introduction to Programming\"\n        provider=\"Tech University\"\n        description=\"Learn the basics of programming\"\n        isAccessibleForFree={false}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Course\");\n    expect(jsonData.name).toBe(\"Introduction to Programming\");\n    expect(jsonData.provider).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Tech University\",\n    });\n  });\n\n  it(\"renders HowTo type\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"HowTo\"\n        name=\"How to Bake a Cake\"\n        description=\"Step by step guide to baking\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toBe(\"HowTo\");\n    expect(jsonData.name).toBe(\"How to Bake a Cake\");\n  });\n\n  it(\"renders Message type\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"Message\"\n        name=\"Important Announcement\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toBe(\"Message\");\n  });\n\n  it(\"renders Review type with rating\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"Review\"\n        name=\"Product Review\"\n        itemReviewed=\"Amazing Product\"\n        reviewRating={{\n          ratingValue: 4.5,\n          bestRating: 5,\n        }}\n        author=\"Jane Smith\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Review\");\n    expect(jsonData.itemReviewed).toBe(\"Amazing Product\");\n    expect(jsonData.reviewRating).toEqual({\n      ratingValue: 4.5,\n      bestRating: 5,\n    });\n  });\n\n  it(\"renders WebPage type\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"WebPage\"\n        name=\"About Us\"\n        url=\"https://example.com/about\"\n        description=\"Learn more about our company\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toBe(\"WebPage\");\n    expect(jsonData.url).toBe(\"https://example.com/about\");\n  });\n\n  it(\"handles string author\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        name=\"Test Work\"\n        author=\"John Doe\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n    });\n  });\n\n  it(\"handles multiple authors\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        name=\"Collaborative Work\"\n        author={[\n          \"John Doe\",\n          { name: \"Jane Smith\", url: \"https://example.com/jane\" },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.author).toEqual([\n      { \"@type\": \"Person\", name: \"John Doe\" },\n      {\n        \"@type\": \"Person\",\n        name: \"Jane Smith\",\n        url: \"https://example.com/jane\",\n      },\n    ]);\n  });\n\n  it(\"handles organization author\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        name=\"Corporate Publication\"\n        author={{\n          name: \"ACME Corp\",\n          logo: \"https://example.com/logo.png\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.author[\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.author.name).toBe(\"ACME Corp\");\n  });\n\n  it(\"handles string image\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        name=\"Visual Content\"\n        image=\"https://example.com/image.jpg\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.image).toBe(\"https://example.com/image.jpg\");\n  });\n\n  it(\"handles ImageObject\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        name=\"Visual Content\"\n        image={{\n          url: \"https://example.com/image.jpg\",\n          width: 800,\n          height: 600,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.image).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/image.jpg\",\n      width: 800,\n      height: 600,\n    });\n  });\n\n  it(\"handles multiple images\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        name=\"Gallery\"\n        image={[\n          \"https://example.com/image1.jpg\",\n          {\n            url: \"https://example.com/image2.jpg\",\n            width: 800,\n            height: 600,\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.image).toHaveLength(2);\n    expect(jsonData.image[0]).toBe(\"https://example.com/image1.jpg\");\n    expect(jsonData.image[1][\"@type\"]).toBe(\"ImageObject\");\n  });\n\n  it(\"handles publisher\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"Article\"\n        headline=\"Published Article\"\n        publisher=\"Publishing House\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.publisher).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Publishing House\",\n    });\n  });\n\n  it(\"handles mainEntityOfPage as string\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        name=\"Main Content\"\n        mainEntityOfPage=\"https://example.com/main\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.mainEntityOfPage).toBe(\"https://example.com/main\");\n  });\n\n  it(\"handles mainEntityOfPage as WebPage\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        name=\"Main Content\"\n        mainEntityOfPage={{\n          \"@id\": \"https://example.com/main\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.mainEntityOfPage).toEqual({\n      \"@type\": \"WebPage\",\n      \"@id\": \"https://example.com/main\",\n    });\n  });\n\n  it(\"handles isAccessibleForFree as false\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd name=\"Premium Content\" isAccessibleForFree={false} />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.isAccessibleForFree).toBe(false);\n  });\n\n  it(\"handles custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        name=\"Custom Script\"\n        scriptId=\"custom-id\"\n        scriptKey=\"custom-key\"\n      />,\n    );\n\n    const script = container.querySelector(\"#custom-id\");\n    expect(script).toBeTruthy();\n    expect(script?.getAttribute(\"data-testid\")).toBe(\"custom-id\");\n  });\n\n  it(\"uses headline over name when both are provided\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"Article\"\n        headline=\"Article Headline\"\n        name=\"Article Name\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.headline).toBe(\"Article Headline\");\n    expect(jsonData.name).toBeUndefined();\n  });\n\n  it(\"dateModified defaults to datePublished for Article types\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"Article\"\n        headline=\"Test Article\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.dateModified).toBe(\"2024-01-01T00:00:00.000Z\");\n  });\n\n  it(\"dateModified does not default for non-Article types\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"HowTo\"\n        name=\"How to Guide\"\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.dateModified).toBeUndefined();\n  });\n\n  it(\"handles all properties combined\", () => {\n    const { container } = render(\n      <CreativeWorkJsonLd\n        type=\"Article\"\n        headline=\"Complete Article\"\n        url=\"https://example.com/article\"\n        author={[\n          \"John Doe\",\n          { name: \"Jane Smith\", url: \"https://example.com/jane\" },\n        ]}\n        datePublished=\"2024-01-01T00:00:00.000Z\"\n        dateModified=\"2024-01-02T00:00:00.000Z\"\n        image={[\n          \"https://example.com/image1.jpg\",\n          { url: \"https://example.com/image2.jpg\", width: 800, height: 600 },\n        ]}\n        publisher={{\n          name: \"Publishing House\",\n          logo: \"https://example.com/logo.png\",\n        }}\n        description=\"A comprehensive article with all properties\"\n        isAccessibleForFree={false}\n        hasPart={[\n          { isAccessibleForFree: false, cssSelector: \".premium1\" },\n          { isAccessibleForFree: false, cssSelector: \".premium2\" },\n        ]}\n        mainEntityOfPage=\"https://example.com/main\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Article\");\n    expect(jsonData.headline).toBe(\"Complete Article\");\n    expect(jsonData.author).toHaveLength(2);\n    expect(jsonData.image).toHaveLength(2);\n    expect(jsonData.hasPart).toHaveLength(2);\n    expect(jsonData.isAccessibleForFree).toBe(false);\n  });\n});\n"
  },
  {
    "path": "src/components/CreativeWorkJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { CreativeWorkJsonLdProps } from \"~/types/creativework.types\";\nimport {\n  processAuthor,\n  processImage,\n  processPublisher,\n  processMainEntityOfPage,\n  processWebPageElement,\n} from \"~/utils/processors\";\n\nexport default function CreativeWorkJsonLd({\n  type = \"CreativeWork\",\n  scriptId,\n  scriptKey,\n  headline,\n  name,\n  url,\n  author,\n  datePublished,\n  dateModified,\n  image,\n  publisher,\n  description,\n  isAccessibleForFree,\n  hasPart,\n  mainEntityOfPage,\n  // Additional properties for specific types\n  text, // For Comment\n  provider, // For Course\n  itemReviewed, // For Review\n  reviewRating, // For Review\n}: CreativeWorkJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": type,\n    // Use headline if provided, otherwise use name\n    ...(headline && { headline }),\n    ...(name && !headline && { name }),\n    ...(url && { url }),\n    ...(author && {\n      author: Array.isArray(author)\n        ? author.map(processAuthor)\n        : processAuthor(author),\n    }),\n    ...(datePublished && { datePublished }),\n    ...(dateModified && { dateModified }),\n    // If dateModified is not provided but datePublished is, use datePublished for certain types\n    ...(!dateModified &&\n      datePublished &&\n      [\"Article\", \"NewsArticle\", \"BlogPosting\"].includes(type) && {\n        dateModified: datePublished,\n      }),\n    ...(image && {\n      image: Array.isArray(image)\n        ? image.map(processImage)\n        : processImage(image),\n    }),\n    ...(publisher && { publisher: processPublisher(publisher) }),\n    ...(description && { description }),\n    ...(isAccessibleForFree !== undefined && { isAccessibleForFree }),\n    ...(hasPart && {\n      hasPart: Array.isArray(hasPart)\n        ? hasPart.map(processWebPageElement)\n        : processWebPageElement(hasPart),\n    }),\n    ...(mainEntityOfPage && {\n      mainEntityOfPage: processMainEntityOfPage(mainEntityOfPage),\n    }),\n    // Type-specific properties\n    ...(text && type === \"Comment\" && { text }),\n    ...(provider &&\n      type === \"Course\" && { provider: processPublisher(provider) }),\n    ...(itemReviewed && type === \"Review\" && { itemReviewed }),\n    ...(reviewRating && type === \"Review\" && { reviewRating }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || `creativework-jsonld-${type.toLowerCase()}`}\n    />\n  );\n}\n\nexport type { CreativeWorkJsonLdProps };\n"
  },
  {
    "path": "src/components/DatasetJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport DatasetJsonLd from \"./DatasetJsonLd\";\n\ndescribe(\"DatasetJsonLd\", () => {\n  it(\"renders basic Dataset with minimal props\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"NCDC Storm Events Database\"\n        description=\"Storm Data is provided by the National Weather Service (NWS)\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Dataset\",\n      name: \"NCDC Storm Events Database\",\n      description:\n        \"Storm Data is provided by the National Weather Service (NWS)\",\n    });\n  });\n\n  it(\"handles string creator\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        creator=\"John Doe\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.creator).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n    });\n  });\n\n  it(\"handles multiple creators\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        creator={[\n          \"Jane Smith\",\n          {\n            name: \"Research Institute\",\n            logo: \"https://example.com/logo.png\",\n          },\n        ]}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.creator).toEqual([\n      {\n        \"@type\": \"Person\",\n        name: \"Jane Smith\",\n      },\n      {\n        \"@type\": \"Organization\",\n        name: \"Research Institute\",\n        logo: \"https://example.com/logo.png\",\n      },\n    ]);\n  });\n\n  it(\"handles spatial coverage as string\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        spatialCoverage=\"Tahoe City, CA\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.spatialCoverage).toBe(\"Tahoe City, CA\");\n  });\n\n  it(\"handles spatial coverage with GeoCoordinates\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        spatialCoverage={{\n          name: \"Test Location\",\n          geo: {\n            latitude: 39.328,\n            longitude: 120.1633,\n          },\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.spatialCoverage).toEqual({\n      \"@type\": \"Place\",\n      name: \"Test Location\",\n      geo: {\n        \"@type\": \"GeoCoordinates\",\n        latitude: 39.328,\n        longitude: 120.1633,\n      },\n    });\n  });\n\n  it(\"handles spatial coverage with GeoShape\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        spatialCoverage={{\n          geo: {\n            box: \"39.3280 120.1633 40.445 123.7878\",\n          },\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.spatialCoverage).toEqual({\n      \"@type\": \"Place\",\n      geo: {\n        \"@type\": \"GeoShape\",\n        box: \"39.3280 120.1633 40.445 123.7878\",\n      },\n    });\n  });\n\n  it(\"handles single distribution\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        distribution={{\n          contentUrl: \"https://example.com/data.csv\",\n          encodingFormat: \"CSV\",\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.distribution).toEqual({\n      \"@type\": \"DataDownload\",\n      contentUrl: \"https://example.com/data.csv\",\n      encodingFormat: \"CSV\",\n    });\n  });\n\n  it(\"handles multiple distributions\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        distribution={[\n          {\n            contentUrl: \"https://example.com/data.csv\",\n            encodingFormat: \"CSV\",\n          },\n          {\n            contentUrl: \"https://example.com/data.json\",\n            encodingFormat: \"JSON\",\n          },\n        ]}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.distribution).toEqual([\n      {\n        \"@type\": \"DataDownload\",\n        contentUrl: \"https://example.com/data.csv\",\n        encodingFormat: \"CSV\",\n      },\n      {\n        \"@type\": \"DataDownload\",\n        contentUrl: \"https://example.com/data.json\",\n        encodingFormat: \"JSON\",\n      },\n    ]);\n  });\n\n  it(\"handles string identifier\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        identifier=\"https://doi.org/10.1000/182\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.identifier).toBe(\"https://doi.org/10.1000/182\");\n  });\n\n  it(\"handles PropertyValue identifier\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        identifier={{\n          value: \"10.1000/182\",\n          propertyID: \"DOI\",\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.identifier).toEqual({\n      \"@type\": \"PropertyValue\",\n      value: \"10.1000/182\",\n      propertyID: \"DOI\",\n    });\n  });\n\n  it(\"handles array of identifiers\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        identifier={[\n          \"https://doi.org/10.1000/182\",\n          {\n            value: \"ark:/12345/fk1234\",\n            propertyID: \"ARK\",\n          },\n        ]}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.identifier).toEqual([\n      \"https://doi.org/10.1000/182\",\n      {\n        \"@type\": \"PropertyValue\",\n        value: \"ark:/12345/fk1234\",\n        propertyID: \"ARK\",\n      },\n    ]);\n  });\n\n  it(\"handles string license\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        license=\"https://creativecommons.org/publicdomain/zero/1.0/\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.license).toBe(\n      \"https://creativecommons.org/publicdomain/zero/1.0/\",\n    );\n  });\n\n  it(\"handles CreativeWork license\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        license={{\n          name: \"Custom License\",\n          url: \"https://example.com/license\",\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.license).toEqual({\n      \"@type\": \"CreativeWork\",\n      name: \"Custom License\",\n      url: \"https://example.com/license\",\n    });\n  });\n\n  it(\"handles variableMeasured as string\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        variableMeasured=\"temperature\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.variableMeasured).toBe(\"temperature\");\n  });\n\n  it(\"handles variableMeasured as PropertyValue\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        variableMeasured={{\n          name: \"Temperature\",\n          value: \"celsius\",\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.variableMeasured).toEqual({\n      \"@type\": \"PropertyValue\",\n      name: \"Temperature\",\n      value: \"celsius\",\n    });\n  });\n\n  it(\"handles variableMeasured as array\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        variableMeasured={[\n          \"temperature\",\n          {\n            name: \"Pressure\",\n            value: \"pascals\",\n          },\n        ]}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.variableMeasured).toEqual([\n      \"temperature\",\n      {\n        \"@type\": \"PropertyValue\",\n        name: \"Pressure\",\n        value: \"pascals\",\n      },\n    ]);\n  });\n\n  it(\"handles isAccessibleForFree as false\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        isAccessibleForFree={false}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.isAccessibleForFree).toBe(false);\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Complete Dataset\"\n        description=\"A comprehensive dataset with all properties\"\n        url=\"https://example.com/dataset\"\n        sameAs={[\"https://other.com/dataset\", \"https://another.com/dataset\"]}\n        identifier=\"https://doi.org/10.1000/182\"\n        keywords={[\"climate\", \"weather\", \"storm\"]}\n        license=\"https://creativecommons.org/publicdomain/zero/1.0/\"\n        isAccessibleForFree={true}\n        creator={{\n          name: \"NOAA\",\n          url: \"https://www.noaa.gov/\",\n          logo: \"https://www.noaa.gov/logo.png\",\n        }}\n        funder=\"National Science Foundation\"\n        includedInDataCatalog={{\n          name: \"data.gov\",\n          url: \"https://data.gov\",\n        }}\n        distribution={{\n          contentUrl: \"https://example.com/data.csv\",\n          encodingFormat: \"CSV\",\n        }}\n        temporalCoverage=\"1950-01-01/2013-12-18\"\n        spatialCoverage=\"United States\"\n        alternateName={[\"Storm Database\", \"Weather Events\"]}\n        citation=\"Doe J (2014) Analysis of storm data\"\n        measurementTechnique=\"Satellite observation\"\n        variableMeasured=\"wind speed\"\n        version=\"2.0\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData).toMatchObject({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Dataset\",\n      name: \"Complete Dataset\",\n      description: \"A comprehensive dataset with all properties\",\n      url: \"https://example.com/dataset\",\n      sameAs: [\"https://other.com/dataset\", \"https://another.com/dataset\"],\n      identifier: \"https://doi.org/10.1000/182\",\n      keywords: [\"climate\", \"weather\", \"storm\"],\n      license: \"https://creativecommons.org/publicdomain/zero/1.0/\",\n      isAccessibleForFree: true,\n      temporalCoverage: \"1950-01-01/2013-12-18\",\n      spatialCoverage: \"United States\",\n      alternateName: [\"Storm Database\", \"Weather Events\"],\n      citation: \"Doe J (2014) Analysis of storm data\",\n      measurementTechnique: \"Satellite observation\",\n      variableMeasured: \"wind speed\",\n      version: \"2.0\",\n    });\n\n    expect(jsonData.creator).toEqual({\n      \"@type\": \"Organization\",\n      name: \"NOAA\",\n      url: \"https://www.noaa.gov/\",\n      logo: \"https://www.noaa.gov/logo.png\",\n    });\n\n    expect(jsonData.funder).toEqual({\n      \"@type\": \"Organization\",\n      name: \"National Science Foundation\",\n    });\n  });\n\n  it(\"handles custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Test Dataset\"\n        description=\"Test description\"\n        scriptId=\"custom-id\"\n        scriptKey=\"custom-key\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script?.getAttribute(\"id\")).toBe(\"custom-id\");\n    expect(script?.getAttribute(\"data-testid\")).toBe(\"custom-id\");\n  });\n\n  it(\"handles hasPart property\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Parent Dataset\"\n        description=\"A dataset with parts\"\n        hasPart={[\n          {\n            \"@type\": \"Dataset\",\n            name: \"Part 1\",\n            description: \"First part of the dataset\",\n          },\n          {\n            \"@type\": \"Dataset\",\n            name: \"Part 2\",\n            description: \"Second part of the dataset\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.hasPart).toHaveLength(2);\n    expect(jsonData.hasPart[0]).toEqual({\n      \"@type\": \"Dataset\",\n      name: \"Part 1\",\n      description: \"First part of the dataset\",\n    });\n    expect(jsonData.hasPart[1]).toEqual({\n      \"@type\": \"Dataset\",\n      name: \"Part 2\",\n      description: \"Second part of the dataset\",\n    });\n  });\n\n  it(\"handles isPartOf property\", () => {\n    const { container } = render(\n      <DatasetJsonLd\n        name=\"Sub Dataset\"\n        description=\"A dataset that is part of another\"\n        isPartOf={{\n          \"@type\": \"Dataset\",\n          name: \"Parent Dataset\",\n          description: \"The parent dataset containing multiple sub-datasets\",\n          url: \"https://example.com/parent-dataset\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.isPartOf).toEqual({\n      \"@type\": \"Dataset\",\n      name: \"Parent Dataset\",\n      description: \"The parent dataset containing multiple sub-datasets\",\n      url: \"https://example.com/parent-dataset\",\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/DatasetJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { DatasetJsonLdProps } from \"~/types/dataset.types\";\nimport {\n  processCreator,\n  processFunder,\n  processIdentifier,\n  processSpatialCoverage,\n  processDataDownload,\n  processLicense,\n  processDataCatalog,\n} from \"~/utils/processors\";\n\nexport default function DatasetJsonLd({\n  scriptId,\n  scriptKey,\n  name,\n  description,\n  url,\n  sameAs,\n  identifier,\n  keywords,\n  license,\n  isAccessibleForFree,\n  hasPart,\n  isPartOf,\n  creator,\n  funder,\n  includedInDataCatalog,\n  distribution,\n  temporalCoverage,\n  spatialCoverage,\n  alternateName,\n  citation,\n  measurementTechnique,\n  variableMeasured,\n  version,\n}: DatasetJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Dataset\",\n    name,\n    description,\n    ...(url && { url }),\n    ...(sameAs && { sameAs }),\n    ...(identifier && {\n      identifier: Array.isArray(identifier)\n        ? identifier.map(processIdentifier)\n        : processIdentifier(identifier),\n    }),\n    ...(keywords && { keywords }),\n    ...(license && { license: processLicense(license) }),\n    ...(isAccessibleForFree !== undefined && { isAccessibleForFree }),\n    ...(hasPart && { hasPart }),\n    ...(isPartOf && { isPartOf }),\n    ...(creator && { creator: processCreator(creator) }),\n    ...(funder && { funder: processFunder(funder) }),\n    ...(includedInDataCatalog && {\n      includedInDataCatalog: processDataCatalog(includedInDataCatalog),\n    }),\n    ...(distribution && {\n      distribution: Array.isArray(distribution)\n        ? distribution.map(processDataDownload)\n        : processDataDownload(distribution),\n    }),\n    ...(temporalCoverage && { temporalCoverage }),\n    ...(spatialCoverage && {\n      spatialCoverage: processSpatialCoverage(spatialCoverage),\n    }),\n    ...(alternateName && { alternateName }),\n    ...(citation && { citation }),\n    ...(measurementTechnique && { measurementTechnique }),\n    ...(variableMeasured && {\n      variableMeasured: Array.isArray(variableMeasured)\n        ? variableMeasured.map((v) =>\n            typeof v === \"string\" ? v : processIdentifier(v),\n          )\n        : typeof variableMeasured === \"string\"\n          ? variableMeasured\n          : processIdentifier(variableMeasured),\n    }),\n    ...(version && { version }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"dataset-jsonld\"}\n    />\n  );\n}\n\nexport type { DatasetJsonLdProps };\n"
  },
  {
    "path": "src/components/DiscussionForumPostingJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport DiscussionForumPostingJsonLd from \"./DiscussionForumPostingJsonLd\";\n\ndescribe(\"DiscussionForumPostingJsonLd\", () => {\n  it(\"renders basic DiscussionForumPosting with minimal props\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author=\"John Doe\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"This is a forum post\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"DiscussionForumPosting\",\n      text: \"This is a forum post\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"John Doe\",\n      },\n      datePublished: \"2024-01-01T08:00:00+00:00\",\n    });\n  });\n\n  it(\"renders SocialMediaPosting type when specified\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        type=\"SocialMediaPosting\"\n        author=\"Jane Smith\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"This is a social media post\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toBe(\"SocialMediaPosting\");\n  });\n\n  it(\"handles string author\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author=\"Katie Pope\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Look at this cool content!\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Katie Pope\",\n    });\n  });\n\n  it(\"handles Person author object\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author={{\n          \"@type\": \"Person\",\n          name: \"Katie Pope\",\n          url: \"https://example.com/user/katie-pope\",\n        }}\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Look at this cool content!\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Katie Pope\",\n      url: \"https://example.com/user/katie-pope\",\n    });\n  });\n\n  it(\"handles Organization author\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author={{\n          \"@type\": \"Organization\",\n          name: \"Example Forum\",\n          url: \"https://example.com\",\n        }}\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Official announcement\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Example Forum\",\n      url: \"https://example.com\",\n    });\n  });\n\n  it(\"handles multiple authors\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author={[\n          \"John Doe\",\n          {\n            \"@type\": \"Person\",\n            name: \"Jane Smith\",\n            url: \"https://example.com/jane\",\n          },\n        ]}\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Co-authored post\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.author).toEqual([\n      {\n        \"@type\": \"Person\",\n        name: \"John Doe\",\n      },\n      {\n        \"@type\": \"Person\",\n        name: \"Jane Smith\",\n        url: \"https://example.com/jane\",\n      },\n    ]);\n  });\n\n  it(\"handles nested comments\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        headline=\"Very Popular Thread\"\n        author=\"Katie Pope\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Look at how cool this concert was!\"\n        comment={[\n          {\n            text: \"Who's the person you're with?\",\n            author: \"Saul Douglas\",\n            datePublished: \"2024-01-01T09:46:02+02:00\",\n          },\n          {\n            text: \"That's my mom, isn't she cool?\",\n            author: {\n              name: \"Katie Pope\",\n              url: \"https://example.com/user/katie-pope\",\n            },\n            datePublished: \"2024-01-01T09:50:25+02:00\",\n            interactionStatistic: {\n              interactionType: \"https://schema.org/LikeAction\",\n              userInteractionCount: 7,\n            },\n            comment: [\n              {\n                text: \"Yes, she is very cool!\",\n                author: \"Another User\",\n                datePublished: \"2024-01-01T10:00:00+02:00\",\n              },\n            ],\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.comment).toHaveLength(2);\n    expect(jsonData.comment[0]).toEqual({\n      \"@type\": \"Comment\",\n      text: \"Who's the person you're with?\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"Saul Douglas\",\n      },\n      datePublished: \"2024-01-01T09:46:02+02:00\",\n    });\n\n    expect(jsonData.comment[1].comment).toHaveLength(1);\n    expect(jsonData.comment[1].comment[0]).toEqual({\n      \"@type\": \"Comment\",\n      text: \"Yes, she is very cool!\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"Another User\",\n      },\n      datePublished: \"2024-01-01T10:00:00+02:00\",\n    });\n  });\n\n  it(\"handles interaction statistics\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author=\"John Doe\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Popular post\"\n        interactionStatistic={[\n          {\n            interactionType: \"https://schema.org/LikeAction\",\n            userInteractionCount: 27,\n          },\n          {\n            interactionType: \"https://schema.org/CommentAction\",\n            userInteractionCount: 5,\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.interactionStatistic).toEqual([\n      {\n        \"@type\": \"InteractionCounter\",\n        interactionType: \"https://schema.org/LikeAction\",\n        userInteractionCount: 27,\n      },\n      {\n        \"@type\": \"InteractionCounter\",\n        interactionType: \"https://schema.org/CommentAction\",\n        userInteractionCount: 5,\n      },\n    ]);\n  });\n\n  it(\"handles single interaction statistic\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author=\"John Doe\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Popular post\"\n        interactionStatistic={{\n          interactionType: \"https://schema.org/ViewAction\",\n          userInteractionCount: 1000,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.interactionStatistic).toEqual({\n      \"@type\": \"InteractionCounter\",\n      interactionType: \"https://schema.org/ViewAction\",\n      userInteractionCount: 1000,\n    });\n  });\n\n  it(\"handles video content\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author=\"Video Creator\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        video={{\n          name: \"Video of concert\",\n          contentUrl: \"https://example.com/media/super-cool-concert.mp4\",\n          uploadDate: \"2024-03-01T06:34:34+02:00\",\n          thumbnailUrl: \"https://example.com/media/super-cool-concert-snap.jpg\",\n          description: \"Amazing concert footage\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.video).toEqual({\n      \"@type\": \"VideoObject\",\n      name: \"Video of concert\",\n      contentUrl: \"https://example.com/media/super-cool-concert.mp4\",\n      uploadDate: \"2024-03-01T06:34:34+02:00\",\n      thumbnailUrl: \"https://example.com/media/super-cool-concert-snap.jpg\",\n      description: \"Amazing concert footage\",\n    });\n  });\n\n  it(\"handles image content\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author=\"Photographer\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Check out these photos\"\n        image={[\n          \"https://example.com/photo1.jpg\",\n          {\n            url: \"https://example.com/photo2.jpg\",\n            width: 1200,\n            height: 800,\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.image).toEqual([\n      \"https://example.com/photo1.jpg\",\n      {\n        \"@type\": \"ImageObject\",\n        url: \"https://example.com/photo2.jpg\",\n        width: 1200,\n        height: 800,\n      },\n    ]);\n  });\n\n  it(\"handles shared content as URL string\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author=\"Link Sharer\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Check out this article\"\n        sharedContent=\"https://example.com/external-article\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.sharedContent).toEqual({\n      \"@type\": \"WebPage\",\n      url: \"https://example.com/external-article\",\n    });\n  });\n\n  it(\"handles shared content as WebPage object\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author=\"Link Sharer\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Check out this article\"\n        sharedContent={{\n          url: \"https://example.com/external-article\",\n          name: \"Amazing Article\",\n          description: \"An article about amazing things\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.sharedContent).toEqual({\n      \"@type\": \"WebPage\",\n      url: \"https://example.com/external-article\",\n      name: \"Amazing Article\",\n      description: \"An article about amazing things\",\n    });\n  });\n\n  it(\"handles deleted posts\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author=\"Deleted User\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"[This post has been deleted]\"\n        creativeWorkStatus=\"Deleted\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.creativeWorkStatus).toBe(\"Deleted\");\n  });\n\n  it(\"handles isPartOf property\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author=\"Forum User\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Post in gaming subforum\"\n        isPartOf={{\n          name: \"Gaming Subforum\",\n          url: \"https://example.com/forums/gaming\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.isPartOf).toEqual({\n      \"@type\": \"CreativeWork\",\n      name: \"Gaming Subforum\",\n      url: \"https://example.com/forums/gaming\",\n    });\n  });\n\n  it(\"handles all properties combined\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        headline=\"I went to the concert!\"\n        text=\"Look at how cool this concert was!\"\n        image=\"https://example.com/concert.jpg\"\n        video={{\n          name: \"Video of concert\",\n          contentUrl: \"https://example.com/media/super-cool-concert.mp4\",\n          uploadDate: \"2024-03-01T06:34:34+02:00\",\n          thumbnailUrl: \"https://example.com/media/super-cool-concert-snap.jpg\",\n          description: \"Concert footage\",\n        }}\n        url=\"https://example.com/post/very-popular-thread\"\n        author={{\n          name: \"Katie Pope\",\n          url: \"https://example.com/user/katie-pope\",\n        }}\n        datePublished=\"2024-03-01T08:34:34+02:00\"\n        dateModified=\"2024-03-01T09:00:00+02:00\"\n        interactionStatistic={{\n          interactionType: \"https://schema.org/LikeAction\",\n          userInteractionCount: 27,\n        }}\n        comment={[\n          {\n            text: \"Who's the person you're with?\",\n            author: {\n              name: \"Saul Douglas\",\n              url: \"https://example.com/user/saul-douglas\",\n            },\n            datePublished: \"2024-03-01T09:46:02+02:00\",\n          },\n        ]}\n        isPartOf=\"https://example.com/forums/concerts\"\n        sharedContent={{\n          url: \"https://example.com/concert-info\",\n          name: \"Concert Information\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData).toMatchObject({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"DiscussionForumPosting\",\n      headline: \"I went to the concert!\",\n      text: \"Look at how cool this concert was!\",\n      url: \"https://example.com/post/very-popular-thread\",\n      datePublished: \"2024-03-01T08:34:34+02:00\",\n      dateModified: \"2024-03-01T09:00:00+02:00\",\n    });\n    expect(jsonData.author).toBeDefined();\n    expect(jsonData.video).toBeDefined();\n    expect(jsonData.comment).toHaveLength(1);\n    expect(jsonData.interactionStatistic).toBeDefined();\n    expect(jsonData.sharedContent).toBeDefined();\n  });\n\n  it(\"uses custom scriptId when provided\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author=\"John Doe\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Forum post\"\n        scriptId=\"custom-forum-id\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script!.getAttribute(\"id\")).toBe(\"custom-forum-id\");\n    expect(script!.getAttribute(\"data-testid\")).toBe(\"custom-forum-id\");\n  });\n\n  it(\"uses custom scriptKey when provided\", () => {\n    const { container } = render(\n      <DiscussionForumPostingJsonLd\n        author=\"John Doe\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        text=\"Forum post\"\n        scriptKey=\"custom-key\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    // React doesn't expose the key prop as an attribute, so we can't test it directly\n    expect(script).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "src/components/DiscussionForumPostingJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { DiscussionForumPostingJsonLdProps } from \"~/types/discussionforum.types\";\nimport {\n  processAuthor,\n  processImage,\n  processVideo,\n  processComment,\n  processInteractionStatistic,\n  processSharedContent,\n  processIsPartOf,\n} from \"~/utils/processors\";\n\nexport default function DiscussionForumPostingJsonLd({\n  type = \"DiscussionForumPosting\",\n  scriptId,\n  scriptKey,\n  headline,\n  text,\n  image,\n  video,\n  author,\n  datePublished,\n  dateModified,\n  url,\n  comment,\n  creativeWorkStatus,\n  interactionStatistic,\n  isPartOf,\n  sharedContent,\n}: DiscussionForumPostingJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": type,\n    ...(headline && { headline }),\n    ...(text && { text }),\n    ...(image && {\n      image: Array.isArray(image)\n        ? image.map(processImage)\n        : processImage(image),\n    }),\n    ...(video && { video: processVideo(video) }),\n    author: Array.isArray(author)\n      ? author.map(processAuthor)\n      : processAuthor(author),\n    datePublished,\n    ...(dateModified && { dateModified }),\n    ...(url && { url }),\n    ...(comment && {\n      comment: comment.map(processComment),\n    }),\n    ...(creativeWorkStatus && { creativeWorkStatus }),\n    ...(interactionStatistic && {\n      interactionStatistic: Array.isArray(interactionStatistic)\n        ? interactionStatistic.map(processInteractionStatistic)\n        : processInteractionStatistic(interactionStatistic),\n    }),\n    ...(isPartOf && { isPartOf: processIsPartOf(isPartOf) }),\n    ...(sharedContent && {\n      sharedContent: processSharedContent(sharedContent),\n    }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || `${type.toLowerCase()}-jsonld`}\n    />\n  );\n}\n\nexport type { DiscussionForumPostingJsonLdProps };\n"
  },
  {
    "path": "src/components/EmployerAggregateRatingJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport EmployerAggregateRatingJsonLd from \"./EmployerAggregateRatingJsonLd\";\n\ndescribe(\"EmployerAggregateRatingJsonLd\", () => {\n  it(\"renders basic EmployerAggregateRating with minimal props and ratingCount\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        itemReviewed=\"World's Best Coffee Shop\"\n        ratingValue={91}\n        ratingCount={10561}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"EmployerAggregateRating\",\n      itemReviewed: {\n        \"@type\": \"Organization\",\n        name: \"World's Best Coffee Shop\",\n      },\n      ratingValue: 91,\n      ratingCount: 10561,\n    });\n  });\n\n  it(\"renders with reviewCount instead of ratingCount\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        itemReviewed=\"Tech Corp\"\n        ratingValue={4.5}\n        reviewCount={250}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.reviewCount).toBe(250);\n    expect(jsonData.ratingCount).toBeUndefined();\n  });\n\n  it(\"renders with both ratingCount and reviewCount\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        itemReviewed=\"Big Company\"\n        ratingValue={4.2}\n        ratingCount={500}\n        reviewCount={450}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.ratingCount).toBe(500);\n    expect(jsonData.reviewCount).toBe(450);\n  });\n\n  it(\"handles string ratingValue\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        itemReviewed=\"Coffee Shop\"\n        ratingValue=\"60%\"\n        ratingCount={100}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.ratingValue).toBe(\"60%\");\n  });\n\n  it(\"throws error when neither ratingCount nor reviewCount is provided\", () => {\n    expect(() =>\n      render(\n        <EmployerAggregateRatingJsonLd\n          itemReviewed=\"Company\"\n          ratingValue={4}\n        />,\n      ),\n    ).toThrow(\n      \"EmployerAggregateRating requires at least one of ratingCount or reviewCount\",\n    );\n  });\n\n  it(\"handles complex Organization object for itemReviewed\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        itemReviewed={{\n          name: \"World's Best Coffee Shop\",\n          sameAs: \"https://example.com\",\n          url: \"https://example.com\",\n          logo: \"https://example.com/logo.png\",\n          address: {\n            streetAddress: \"123 Main St\",\n            addressLocality: \"Seattle\",\n            addressRegion: \"WA\",\n            postalCode: \"98101\",\n            addressCountry: \"US\",\n          },\n        }}\n        ratingValue={91}\n        ratingCount={10561}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.itemReviewed).toEqual({\n      \"@type\": \"Organization\",\n      name: \"World's Best Coffee Shop\",\n      sameAs: \"https://example.com\",\n      url: \"https://example.com\",\n      logo: \"https://example.com/logo.png\",\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"123 Main St\",\n        addressLocality: \"Seattle\",\n        addressRegion: \"WA\",\n        postalCode: \"98101\",\n        addressCountry: \"US\",\n      },\n    });\n  });\n\n  it(\"handles Organization object with @type already set\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        itemReviewed={{\n          \"@type\": \"Organization\",\n          name: \"Tech Company\",\n          url: \"https://techcompany.com\",\n        }}\n        ratingValue={4.8}\n        ratingCount={1200}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.itemReviewed[\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.itemReviewed.name).toBe(\"Tech Company\");\n  });\n\n  it(\"includes bestRating and worstRating when provided\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        itemReviewed=\"Custom Scale Company\"\n        ratingValue={85}\n        ratingCount={500}\n        bestRating={100}\n        worstRating={0}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.bestRating).toBe(100);\n    expect(jsonData.worstRating).toBe(0);\n  });\n\n  it(\"handles bestRating and worstRating edge cases\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        itemReviewed=\"Edge Case Company\"\n        ratingValue={1}\n        ratingCount={10}\n        bestRating={1}\n        worstRating={1}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.bestRating).toBe(1);\n    expect(jsonData.worstRating).toBe(1);\n  });\n\n  it(\"uses custom scriptId when provided\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        scriptId=\"custom-employer-rating-id\"\n        itemReviewed=\"Company\"\n        ratingValue={4}\n        ratingCount={100}\n      />,\n    );\n\n    const script = container.querySelector(\"#custom-employer-rating-id\");\n    expect(script).toBeTruthy();\n    expect(script?.getAttribute(\"type\")).toBe(\"application/ld+json\");\n    expect(script?.getAttribute(\"data-testid\")).toBe(\n      \"custom-employer-rating-id\",\n    );\n  });\n\n  it(\"uses custom scriptKey when provided\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        scriptKey=\"custom-key\"\n        itemReviewed=\"Company\"\n        ratingValue={4}\n        ratingCount={100}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n    // scriptKey is used for React key, not data-testid\n    // data-testid is only set when scriptId is provided\n    expect(script?.getAttribute(\"data-testid\")).toBeNull();\n    expect(script?.getAttribute(\"id\")).toBe(\"custom-key\");\n  });\n\n  it(\"uses default id when scriptId not provided\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        itemReviewed=\"Company\"\n        ratingValue={4}\n        ratingCount={100}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script?.getAttribute(\"id\")).toBe(\"employer-aggregate-rating-jsonld\");\n    // data-testid is null when scriptId is not provided\n    expect(script?.getAttribute(\"data-testid\")).toBeNull();\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        itemReviewed={{\n          name: \"Full Featured Company\",\n          sameAs: [\n            \"https://facebook.com/company\",\n            \"https://twitter.com/company\",\n          ],\n          url: \"https://company.com\",\n          logo: {\n            url: \"https://company.com/logo.png\",\n            width: 600,\n            height: 300,\n          },\n          description: \"A great place to work\",\n          telephone: \"+1-555-123-4567\",\n          email: \"hr@company.com\",\n          address: [\n            {\n              streetAddress: \"123 Main St\",\n              addressLocality: \"San Francisco\",\n              addressRegion: \"CA\",\n              postalCode: \"94105\",\n              addressCountry: \"US\",\n            },\n            {\n              streetAddress: \"456 Oak Ave\",\n              addressLocality: \"New York\",\n              addressRegion: \"NY\",\n              postalCode: \"10001\",\n              addressCountry: \"US\",\n            },\n          ],\n        }}\n        ratingValue={4.7}\n        ratingCount={1500}\n        reviewCount={1450}\n        bestRating={5}\n        worstRating={1}\n        scriptId=\"full-example\"\n        scriptKey=\"full-example-key\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"EmployerAggregateRating\");\n    expect(jsonData.ratingValue).toBe(4.7);\n    expect(jsonData.ratingCount).toBe(1500);\n    expect(jsonData.reviewCount).toBe(1450);\n    expect(jsonData.bestRating).toBe(5);\n    expect(jsonData.worstRating).toBe(1);\n    expect(jsonData.itemReviewed.name).toBe(\"Full Featured Company\");\n    expect(jsonData.itemReviewed.sameAs).toEqual([\n      \"https://facebook.com/company\",\n      \"https://twitter.com/company\",\n    ]);\n    // Verify nested properties are processed correctly\n    expect(jsonData.itemReviewed.logo).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://company.com/logo.png\",\n      width: 600,\n      height: 300,\n    });\n    expect(jsonData.itemReviewed.address[0][\"@type\"]).toBe(\"PostalAddress\");\n    expect(jsonData.itemReviewed.address[1][\"@type\"]).toBe(\"PostalAddress\");\n  });\n\n  it(\"properly processes nested Organization properties\", () => {\n    const { container } = render(\n      <EmployerAggregateRatingJsonLd\n        itemReviewed={{\n          name: \"Complex Corp\",\n          numberOfEmployees: 1000,\n          contactPoint: {\n            contactType: \"HR Department\",\n            telephone: \"+1-555-HR-DEPT\",\n            email: \"hr@complex-corp.com\",\n          },\n        }}\n        ratingValue={4.5}\n        ratingCount={750}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.itemReviewed.numberOfEmployees).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 1000,\n    });\n    expect(jsonData.itemReviewed.contactPoint).toEqual({\n      \"@type\": \"ContactPoint\",\n      contactType: \"HR Department\",\n      telephone: \"+1-555-HR-DEPT\",\n      email: \"hr@complex-corp.com\",\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/EmployerAggregateRatingJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { EmployerAggregateRatingJsonLdProps } from \"~/types/employer-aggregate-rating.types\";\nimport type { Organization } from \"~/types/common.types\";\nimport {\n  processAddress,\n  processContactPoint,\n  processLogo,\n  processNumberOfEmployees,\n} from \"~/utils/processors\";\n\nfunction processEmployerItemReviewed(\n  itemReviewed: string | Organization | Omit<Organization, \"@type\">,\n): Organization {\n  if (typeof itemReviewed === \"string\") {\n    return {\n      \"@type\": \"Organization\",\n      name: itemReviewed,\n    };\n  }\n\n  // Start with base organization\n  const org: Organization = {\n    \"@type\": \"Organization\",\n    ...itemReviewed,\n  };\n\n  // Process nested properties if present\n  if (\n    \"logo\" in itemReviewed &&\n    itemReviewed.logo &&\n    typeof itemReviewed.logo !== \"string\"\n  ) {\n    org.logo = processLogo(itemReviewed.logo);\n  }\n\n  if (\"address\" in itemReviewed && itemReviewed.address) {\n    if (Array.isArray(itemReviewed.address)) {\n      org.address = itemReviewed.address.map((addr) =>\n        typeof addr === \"string\" ? addr : processAddress(addr),\n      );\n    } else if (typeof itemReviewed.address !== \"string\") {\n      org.address = processAddress(itemReviewed.address);\n    }\n  }\n\n  if (\"contactPoint\" in itemReviewed && itemReviewed.contactPoint) {\n    if (Array.isArray(itemReviewed.contactPoint)) {\n      org.contactPoint = itemReviewed.contactPoint.map(processContactPoint);\n    } else {\n      org.contactPoint = processContactPoint(itemReviewed.contactPoint);\n    }\n  }\n\n  if (\"numberOfEmployees\" in itemReviewed && itemReviewed.numberOfEmployees) {\n    org.numberOfEmployees = processNumberOfEmployees(\n      itemReviewed.numberOfEmployees,\n    );\n  }\n\n  return org;\n}\n\nexport default function EmployerAggregateRatingJsonLd({\n  scriptId,\n  scriptKey,\n  itemReviewed,\n  ratingValue,\n  ratingCount,\n  reviewCount,\n  bestRating,\n  worstRating,\n}: EmployerAggregateRatingJsonLdProps) {\n  // Validate that at least one of ratingCount or reviewCount is provided\n  if (!ratingCount && !reviewCount) {\n    throw new Error(\n      \"EmployerAggregateRating requires at least one of ratingCount or reviewCount\",\n    );\n  }\n\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"EmployerAggregateRating\",\n    itemReviewed: processEmployerItemReviewed(itemReviewed),\n    ratingValue,\n    ...(ratingCount && { ratingCount }),\n    ...(reviewCount && { reviewCount }),\n    ...(bestRating !== undefined && { bestRating }),\n    ...(worstRating !== undefined && { worstRating }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"employer-aggregate-rating-jsonld\"}\n    />\n  );\n}\n\nexport type { EmployerAggregateRatingJsonLdProps };\n"
  },
  {
    "path": "src/components/EventJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport EventJsonLd from \"./EventJsonLd\";\n\ndescribe(\"EventJsonLd\", () => {\n  it(\"renders basic Event with minimal props\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"The Adventures of Kira and Morrison\"\n        startDate=\"2025-07-21T19:00-05:00\"\n        location=\"Snickerpark Stadium\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Event\",\n      name: \"The Adventures of Kira and Morrison\",\n      startDate: \"2025-07-21T19:00-05:00\",\n      location: {\n        \"@type\": \"Place\",\n        name: \"Snickerpark Stadium\",\n        address: {\n          \"@type\": \"PostalAddress\",\n          streetAddress: \"Snickerpark Stadium\",\n        },\n      },\n    });\n  });\n\n  it(\"renders Event with full location object\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Tech Conference 2025\"\n        startDate=\"2025-08-15T09:00:00-08:00\"\n        location={{\n          \"@type\": \"Place\",\n          name: \"Convention Center\",\n          address: {\n            \"@type\": \"PostalAddress\",\n            streetAddress: \"100 Main Street\",\n            addressLocality: \"San Francisco\",\n            addressRegion: \"CA\",\n            postalCode: \"94105\",\n            addressCountry: \"US\",\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.location).toEqual({\n      \"@type\": \"Place\",\n      name: \"Convention Center\",\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"100 Main Street\",\n        addressLocality: \"San Francisco\",\n        addressRegion: \"CA\",\n        postalCode: \"94105\",\n        addressCountry: \"US\",\n      },\n    });\n  });\n\n  it(\"handles string performer\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Comedy Night\"\n        startDate=\"2025-07-21T20:00:00\"\n        location=\"Comedy Club\"\n        performer=\"John Doe\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.performer).toEqual({\n      \"@type\": \"PerformingGroup\",\n      name: \"John Doe\",\n    });\n  });\n\n  it(\"handles multiple performers\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Music Festival\"\n        startDate=\"2025-07-21T14:00:00\"\n        location=\"Festival Grounds\"\n        performer={[\n          \"Band One\",\n          {\n            \"@type\": \"Person\",\n            name: \"Solo Artist\",\n          },\n          {\n            \"@type\": \"PerformingGroup\",\n            name: \"Band Two\",\n          },\n        ]}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.performer).toEqual([\n      {\n        \"@type\": \"PerformingGroup\",\n        name: \"Band One\",\n      },\n      {\n        \"@type\": \"Person\",\n        name: \"Solo Artist\",\n      },\n      {\n        \"@type\": \"PerformingGroup\",\n        name: \"Band Two\",\n      },\n    ]);\n  });\n\n  it(\"handles string organizer\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Charity Run\"\n        startDate=\"2025-09-01T07:00:00\"\n        location=\"City Park\"\n        organizer=\"Local Charity Foundation\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.organizer).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Local Charity Foundation\",\n    });\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Complete Event Example\"\n        startDate=\"2025-07-21T19:00-05:00\"\n        endDate=\"2025-07-21T23:00-05:00\"\n        location={{\n          \"@type\": \"Place\",\n          name: \"Snickerpark Stadium\",\n          address: {\n            \"@type\": \"PostalAddress\",\n            streetAddress: \"100 West Snickerpark Dr\",\n            addressLocality: \"Snickertown\",\n            postalCode: \"19019\",\n            addressRegion: \"PA\",\n            addressCountry: \"US\",\n          },\n        }}\n        description=\"The Adventures of Kira and Morrison is coming to Snickertown in a can't miss performance.\"\n        eventStatus=\"https://schema.org/EventScheduled\"\n        image={[\n          \"https://example.com/photos/1x1/photo.jpg\",\n          \"https://example.com/photos/4x3/photo.jpg\",\n          \"https://example.com/photos/16x9/photo.jpg\",\n        ]}\n        offers={{\n          \"@type\": \"Offer\",\n          url: \"https://www.example.com/event_offer/12345_202403180430\",\n          price: 30,\n          priceCurrency: \"USD\",\n          availability: \"https://schema.org/InStock\",\n          validFrom: \"2024-05-21T12:00\",\n        }}\n        performer={{\n          \"@type\": \"PerformingGroup\",\n          name: \"Kira and Morrison\",\n        }}\n        organizer={{\n          \"@type\": \"Organization\",\n          name: \"Kira and Morrison Music\",\n          url: \"https://kiraandmorrisonmusic.com\",\n        }}\n        url=\"https://example.com/events/kira-morrison\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData).toMatchObject({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Event\",\n      name: \"Complete Event Example\",\n      startDate: \"2025-07-21T19:00-05:00\",\n      endDate: \"2025-07-21T23:00-05:00\",\n      description:\n        \"The Adventures of Kira and Morrison is coming to Snickertown in a can't miss performance.\",\n      eventStatus: \"https://schema.org/EventScheduled\",\n      url: \"https://example.com/events/kira-morrison\",\n    });\n\n    expect(jsonData.image).toEqual([\n      \"https://example.com/photos/1x1/photo.jpg\",\n      \"https://example.com/photos/4x3/photo.jpg\",\n      \"https://example.com/photos/16x9/photo.jpg\",\n    ]);\n  });\n\n  it(\"handles cancelled event status\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Cancelled Concert\"\n        startDate=\"2025-07-21T19:00-05:00\"\n        location=\"Concert Hall\"\n        eventStatus=\"https://schema.org/EventCancelled\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.eventStatus).toBe(\"https://schema.org/EventCancelled\");\n  });\n\n  it(\"handles rescheduled event with previousStartDate\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Rescheduled Conference\"\n        startDate=\"2025-09-15T09:00:00\"\n        location=\"Convention Center\"\n        eventStatus=\"https://schema.org/EventRescheduled\"\n        previousStartDate=\"2025-07-15T09:00:00\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.eventStatus).toBe(\"https://schema.org/EventRescheduled\");\n    expect(jsonData.previousStartDate).toBe(\"2025-07-15T09:00:00\");\n  });\n\n  it(\"handles multiple previous start dates\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Much Rescheduled Event\"\n        startDate=\"2025-12-01T19:00:00\"\n        location=\"Event Space\"\n        eventStatus=\"https://schema.org/EventRescheduled\"\n        previousStartDate={[\n          \"2025-03-01T19:00:00\",\n          \"2025-06-01T19:00:00\",\n          \"2025-09-01T19:00:00\",\n        ]}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.previousStartDate).toEqual([\n      \"2025-03-01T19:00:00\",\n      \"2025-06-01T19:00:00\",\n      \"2025-09-01T19:00:00\",\n    ]);\n  });\n\n  it(\"handles multiple offers\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Tiered Pricing Event\"\n        startDate=\"2025-08-01T19:00:00\"\n        location=\"Theater\"\n        offers={[\n          {\n            \"@type\": \"Offer\",\n            price: 25,\n            priceCurrency: \"USD\",\n            availability: \"https://schema.org/InStock\",\n          },\n          {\n            \"@type\": \"Offer\",\n            price: 50,\n            priceCurrency: \"USD\",\n            availability: \"https://schema.org/InStock\",\n          },\n        ]}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.offers).toHaveLength(2);\n    expect(jsonData.offers[0].price).toBe(25);\n    expect(jsonData.offers[1].price).toBe(50);\n  });\n\n  it(\"handles ImageObject in images\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Event with Rich Images\"\n        startDate=\"2025-07-21T19:00:00\"\n        location=\"Photo Gallery\"\n        image={[\n          \"https://example.com/simple.jpg\",\n          {\n            \"@type\": \"ImageObject\",\n            url: \"https://example.com/detailed.jpg\",\n            width: 1200,\n            height: 800,\n            caption: \"Event poster\",\n          },\n        ]}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.image).toEqual([\n      \"https://example.com/simple.jpg\",\n      {\n        \"@type\": \"ImageObject\",\n        url: \"https://example.com/detailed.jpg\",\n        width: 1200,\n        height: 800,\n        caption: \"Event poster\",\n      },\n    ]);\n  });\n\n  it(\"handles custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Custom ID Event\"\n        startDate=\"2025-07-21T19:00:00\"\n        location=\"Venue\"\n        scriptId=\"my-event-json-ld\"\n        scriptKey=\"event-custom-key\"\n      />,\n    );\n\n    const script = container.querySelector(\"#my-event-json-ld\");\n    expect(script).toBeTruthy();\n  });\n\n  it(\"handles day-long event with date only\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"All Day Festival\"\n        startDate=\"2025-07-04\"\n        endDate=\"2025-07-04\"\n        location=\"City Square\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.startDate).toBe(\"2025-07-04\");\n    expect(jsonData.endDate).toBe(\"2025-07-04\");\n  });\n\n  it(\"handles free event with price 0\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Free Community Event\"\n        startDate=\"2025-08-01T14:00:00\"\n        location=\"Community Center\"\n        offers={{\n          \"@type\": \"Offer\",\n          price: 0,\n          priceCurrency: \"USD\",\n          availability: \"https://schema.org/InStock\",\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.offers.price).toBe(0);\n  });\n\n  it(\"handles single image as string\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Event with Single Image\"\n        startDate=\"2025-07-21T19:00:00\"\n        location=\"Photo Gallery\"\n        image=\"https://example.com/event-poster.jpg\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.image).toBe(\"https://example.com/event-poster.jpg\");\n  });\n\n  it(\"handles single image as ImageObject\", () => {\n    const { container } = render(\n      <EventJsonLd\n        name=\"Event with Single Rich Image\"\n        startDate=\"2025-07-21T19:00:00\"\n        location=\"Photo Gallery\"\n        image={{\n          url: \"https://example.com/event-poster.jpg\",\n          width: 1920,\n          height: 1080,\n          caption: \"Main event poster\",\n        }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n    expect(jsonData.image).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/event-poster.jpg\",\n      width: 1920,\n      height: 1080,\n      caption: \"Main event poster\",\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/EventJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { EventJsonLdProps } from \"~/types/event.types\";\nimport {\n  processImage,\n  processPlace,\n  processPerformer,\n  processOrganizer,\n  processOffer,\n} from \"~/utils/processors\";\n\nexport default function EventJsonLd({\n  scriptId,\n  scriptKey,\n  name,\n  startDate,\n  location,\n  endDate,\n  description,\n  eventStatus,\n  image,\n  offers,\n  performer,\n  organizer,\n  previousStartDate,\n  url,\n}: EventJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Event\",\n    name,\n    startDate,\n    location: processPlace(location),\n    ...(endDate && { endDate }),\n    ...(description && { description }),\n    ...(eventStatus && { eventStatus }),\n    ...(image && {\n      image: Array.isArray(image)\n        ? image.map(processImage)\n        : processImage(image),\n    }),\n    ...(offers && {\n      offers: Array.isArray(offers)\n        ? offers.map(processOffer)\n        : processOffer(offers),\n    }),\n    ...(performer && {\n      performer: Array.isArray(performer)\n        ? performer.map(processPerformer)\n        : processPerformer(performer),\n    }),\n    ...(organizer && {\n      organizer: processOrganizer(organizer),\n    }),\n    ...(previousStartDate && { previousStartDate }),\n    ...(url && { url }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"event-jsonld\"}\n    />\n  );\n}\n\nexport type { EventJsonLdProps };\n"
  },
  {
    "path": "src/components/FAQJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport FAQJsonLd from \"./FAQJsonLd\";\n\ndescribe(\"FAQJsonLd\", () => {\n  it(\"renders basic FAQ with simple question/answer format\", () => {\n    const { container } = render(\n      <FAQJsonLd\n        questions={[\n          {\n            question: \"How to find an apprenticeship?\",\n            answer:\n              \"We provide an official service to search through available apprenticeships.\",\n          },\n          {\n            question: \"Whom to contact?\",\n            answer:\n              \"You can contact the apprenticeship office through our official phone hotline.\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"FAQPage\",\n      mainEntity: [\n        {\n          \"@type\": \"Question\",\n          name: \"How to find an apprenticeship?\",\n          acceptedAnswer: {\n            \"@type\": \"Answer\",\n            text: \"We provide an official service to search through available apprenticeships.\",\n          },\n        },\n        {\n          \"@type\": \"Question\",\n          name: \"Whom to contact?\",\n          acceptedAnswer: {\n            \"@type\": \"Answer\",\n            text: \"You can contact the apprenticeship office through our official phone hotline.\",\n          },\n        },\n      ],\n    });\n  });\n\n  it(\"handles HTML content in answers\", () => {\n    const { container } = render(\n      <FAQJsonLd\n        questions={[\n          {\n            question: \"How to apply?\",\n            answer:\n              \"<p>Follow these steps:</p><ol><li>Create an account</li><li>Fill the form</li><li>Submit</li></ol>\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.mainEntity[0].acceptedAnswer.text).toBe(\n      \"<p>Follow these steps:</p><ol><li>Create an account</li><li>Fill the form</li><li>Submit</li></ol>\",\n    );\n  });\n\n  it(\"handles Schema.org format with name/acceptedAnswer\", () => {\n    const { container } = render(\n      <FAQJsonLd\n        questions={[\n          {\n            name: \"What are the requirements?\",\n            acceptedAnswer:\n              \"You must be at least 18 years old and have a high school diploma.\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.mainEntity[0]).toEqual({\n      \"@type\": \"Question\",\n      name: \"What are the requirements?\",\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: \"You must be at least 18 years old and have a high school diploma.\",\n      },\n    });\n  });\n\n  it(\"handles complex Answer object format\", () => {\n    const { container } = render(\n      <FAQJsonLd\n        questions={[\n          {\n            name: \"What documents are needed?\",\n            acceptedAnswer: {\n              \"@type\": \"Answer\",\n              text: \"<p>You need the following documents:</p><ul><li>ID card</li><li>Diploma</li></ul>\",\n            },\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.mainEntity[0].acceptedAnswer).toEqual({\n      \"@type\": \"Answer\",\n      text: \"<p>You need the following documents:</p><ul><li>ID card</li><li>Diploma</li></ul>\",\n    });\n  });\n\n  it(\"handles string-only questions (with empty answers)\", () => {\n    const { container } = render(\n      <FAQJsonLd\n        questions={[\n          \"What is the application deadline?\",\n          \"How long does the process take?\",\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.mainEntity).toEqual([\n      {\n        \"@type\": \"Question\",\n        name: \"What is the application deadline?\",\n        acceptedAnswer: {\n          \"@type\": \"Answer\",\n          text: \"\",\n        },\n      },\n      {\n        \"@type\": \"Question\",\n        name: \"How long does the process take?\",\n        acceptedAnswer: {\n          \"@type\": \"Answer\",\n          text: \"\",\n        },\n      },\n    ]);\n  });\n\n  it(\"handles empty questions array\", () => {\n    const { container } = render(<FAQJsonLd questions={[]} />);\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"FAQPage\",\n      mainEntity: [],\n    });\n  });\n\n  it(\"supports custom scriptId\", () => {\n    const { container } = render(\n      <FAQJsonLd\n        questions={[\n          {\n            question: \"Test question?\",\n            answer: \"Test answer.\",\n          },\n        ]}\n        scriptId=\"custom-faq-id\"\n      />,\n    );\n\n    const script = container.querySelector(\"#custom-faq-id\");\n    expect(script).toBeTruthy();\n    expect(script?.getAttribute(\"type\")).toBe(\"application/ld+json\");\n  });\n\n  it(\"supports custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <FAQJsonLd\n        questions={[\n          {\n            question: \"Test question?\",\n            answer: \"Test answer.\",\n          },\n        ]}\n        scriptId=\"custom-faq-id\"\n        scriptKey=\"custom-faq-key\"\n      />,\n    );\n\n    // Check for custom scriptId (which sets both id and data-testid)\n    const script = container.querySelector(\"#custom-faq-id\");\n    expect(script).toBeTruthy();\n    expect(script?.getAttribute(\"type\")).toBe(\"application/ld+json\");\n    expect(script?.getAttribute(\"data-testid\")).toBe(\"custom-faq-id\");\n  });\n\n  it(\"handles mixed input formats\", () => {\n    const { container } = render(\n      <FAQJsonLd\n        questions={[\n          \"Just a question with no answer\",\n          {\n            question: \"Simple format question?\",\n            answer: \"Simple format answer.\",\n          },\n          {\n            name: \"Schema.org format question?\",\n            acceptedAnswer: \"Schema.org format answer.\",\n          },\n          {\n            name: \"Complex answer format?\",\n            acceptedAnswer: {\n              \"@type\": \"Answer\",\n              text: \"<p>Complex HTML answer</p>\",\n            },\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.mainEntity).toHaveLength(4);\n    expect(jsonData.mainEntity[0].name).toBe(\"Just a question with no answer\");\n    expect(jsonData.mainEntity[0].acceptedAnswer.text).toBe(\"\");\n    expect(jsonData.mainEntity[1].name).toBe(\"Simple format question?\");\n    expect(jsonData.mainEntity[2].name).toBe(\"Schema.org format question?\");\n    expect(jsonData.mainEntity[3].acceptedAnswer.text).toBe(\n      \"<p>Complex HTML answer</p>\",\n    );\n  });\n});\n"
  },
  {
    "path": "src/components/FAQJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type {\n  FAQJsonLdProps,\n  QuestionInput,\n  Question,\n  Answer,\n} from \"~/types/faq.types\";\n\n// Process flexible question input into proper Question format\nfunction processQuestion(input: QuestionInput): Question {\n  // Handle string input - just the question with empty answer\n  if (typeof input === \"string\") {\n    return {\n      \"@type\": \"Question\",\n      name: input,\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: \"\",\n      },\n    };\n  }\n\n  // Handle simple question/answer format\n  if (\"question\" in input && \"answer\" in input) {\n    return {\n      \"@type\": \"Question\",\n      name: input.question,\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: input.answer,\n      },\n    };\n  }\n\n  // Handle Schema.org format with name/acceptedAnswer\n  if (\"name\" in input) {\n    const acceptedAnswer: Answer =\n      typeof input.acceptedAnswer === \"string\"\n        ? {\n            \"@type\": \"Answer\",\n            text: input.acceptedAnswer,\n          }\n        : input.acceptedAnswer;\n\n    return {\n      \"@type\": \"Question\",\n      name: input.name,\n      acceptedAnswer,\n    };\n  }\n\n  // Should never reach here due to TypeScript, but handle gracefully\n  return {\n    \"@type\": \"Question\",\n    name: \"\",\n    acceptedAnswer: {\n      \"@type\": \"Answer\",\n      text: \"\",\n    },\n  };\n}\n\nexport default function FAQJsonLd({\n  questions,\n  scriptId,\n  scriptKey,\n}: FAQJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"FAQPage\",\n    mainEntity: questions.map(processQuestion),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"faq-jsonld\"}\n    />\n  );\n}\n\nexport type { FAQJsonLdProps };\n"
  },
  {
    "path": "src/components/HowToJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport HowToJsonLd from \"./HowToJsonLd\";\n\ndescribe(\"HowToJsonLd\", () => {\n  it(\"renders basic HowTo with minimal props\", () => {\n    const { container } = render(<HowToJsonLd name=\"How to Change a Tire\" />);\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"HowTo\",\n      name: \"How to Change a Tire\",\n    });\n  });\n\n  it(\"handles string steps\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Simple Guide\"\n        step={[\"Step 1: Do this\", \"Step 2: Do that\"]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.step).toEqual([\"Step 1: Do this\", \"Step 2: Do that\"]);\n  });\n\n  it(\"handles HowToStep objects\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Detailed Guide\"\n        step={[\n          {\n            \"@type\": \"HowToStep\",\n            name: \"First Step\",\n            text: \"Do the first thing\",\n            url: \"https://example.com/step-1\",\n          },\n          {\n            \"@type\": \"HowToStep\",\n            name: \"Second Step\",\n            text: \"Do the second thing\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.step).toEqual([\n      {\n        \"@type\": \"HowToStep\",\n        name: \"First Step\",\n        text: \"Do the first thing\",\n        url: \"https://example.com/step-1\",\n      },\n      {\n        \"@type\": \"HowToStep\",\n        name: \"Second Step\",\n        text: \"Do the second thing\",\n      },\n    ]);\n  });\n\n  it(\"handles HowToStep without @type\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Guide\"\n        step={[\n          {\n            name: \"First Step\",\n            text: \"Do something\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.step[0][\"@type\"]).toBe(\"HowToStep\");\n    expect(jsonData.step[0].name).toBe(\"First Step\");\n    expect(jsonData.step[0].text).toBe(\"Do something\");\n  });\n\n  it(\"handles HowToSection objects\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Sectioned Guide\"\n        step={[\n          {\n            \"@type\": \"HowToSection\",\n            name: \"Preparation\",\n            position: 1,\n            itemListElement: [\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Gather materials\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Prepare workspace\",\n              },\n            ],\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.step[0][\"@type\"]).toBe(\"HowToSection\");\n    expect(jsonData.step[0].name).toBe(\"Preparation\");\n    expect(jsonData.step[0].itemListElement).toHaveLength(2);\n  });\n\n  it(\"handles HowToSection without @type\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Sectioned Guide\"\n        step={[\n          {\n            name: \"Setup\",\n            itemListElement: [\n              {\n                text: \"First step\",\n              },\n            ],\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.step[0][\"@type\"]).toBe(\"HowToSection\");\n    expect(jsonData.step[0].name).toBe(\"Setup\");\n  });\n\n  it(\"handles string supplies\", () => {\n    const { container } = render(\n      <HowToJsonLd name=\"DIY Project\" supply={[\"Wood\", \"Nails\", \"Paint\"]} />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.supply).toEqual([\n      { \"@type\": \"HowToSupply\", name: \"Wood\" },\n      { \"@type\": \"HowToSupply\", name: \"Nails\" },\n      { \"@type\": \"HowToSupply\", name: \"Paint\" },\n    ]);\n  });\n\n  it(\"handles HowToSupply objects\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"DIY Project\"\n        supply={[\n          {\n            \"@type\": \"HowToSupply\",\n            name: \"Wood planks\",\n            image: \"https://example.com/wood.jpg\",\n          },\n          {\n            name: \"Nails\",\n            requiredQuantity: 20,\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.supply[0]).toEqual({\n      \"@type\": \"HowToSupply\",\n      name: \"Wood planks\",\n      image: \"https://example.com/wood.jpg\",\n    });\n    expect(jsonData.supply[1][\"@type\"]).toBe(\"HowToSupply\");\n    expect(jsonData.supply[1].name).toBe(\"Nails\");\n    expect(jsonData.supply[1].requiredQuantity).toBe(20);\n  });\n\n  it(\"handles string tools\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"DIY Project\"\n        tool={[\"Hammer\", \"Screwdriver\", \"Drill\"]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.tool).toEqual([\n      { \"@type\": \"HowToTool\", name: \"Hammer\" },\n      { \"@type\": \"HowToTool\", name: \"Screwdriver\" },\n      { \"@type\": \"HowToTool\", name: \"Drill\" },\n    ]);\n  });\n\n  it(\"handles HowToTool objects\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"DIY Project\"\n        tool={[\n          {\n            \"@type\": \"HowToTool\",\n            name: \"Lug wrench\",\n            image: \"https://example.com/lug-wrench.jpg\",\n          },\n          {\n            name: \"Jack\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.tool[0]).toEqual({\n      \"@type\": \"HowToTool\",\n      name: \"Lug wrench\",\n      image: \"https://example.com/lug-wrench.jpg\",\n    });\n    expect(jsonData.tool[1][\"@type\"]).toBe(\"HowToTool\");\n    expect(jsonData.tool[1].name).toBe(\"Jack\");\n  });\n\n  it(\"handles string estimatedCost\", () => {\n    const { container } = render(\n      <HowToJsonLd name=\"DIY Project\" estimatedCost=\"About $20\" />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.estimatedCost).toBe(\"About $20\");\n  });\n\n  it(\"handles MonetaryAmount estimatedCost\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"DIY Project\"\n        estimatedCost={{\n          \"@type\": \"MonetaryAmount\",\n          currency: \"USD\",\n          value: 20,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.estimatedCost).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      currency: \"USD\",\n      value: 20,\n    });\n  });\n\n  it(\"handles MonetaryAmount estimatedCost without @type\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"DIY Project\"\n        estimatedCost={{\n          currency: \"USD\",\n          value: 50,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.estimatedCost[\"@type\"]).toBe(\"MonetaryAmount\");\n    expect(jsonData.estimatedCost.currency).toBe(\"USD\");\n    expect(jsonData.estimatedCost.value).toBe(50);\n  });\n\n  it(\"handles duration properties\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Timed Guide\"\n        prepTime=\"PT5M\"\n        performTime=\"PT25M\"\n        totalTime=\"PT30M\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.prepTime).toBe(\"PT5M\");\n    expect(jsonData.performTime).toBe(\"PT25M\");\n    expect(jsonData.totalTime).toBe(\"PT30M\");\n  });\n\n  it(\"handles string yield\", () => {\n    const { container } = render(\n      <HowToJsonLd name=\"Craft Project\" yield=\"1 finished birdhouse\" />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.yield).toBe(\"1 finished birdhouse\");\n  });\n\n  it(\"handles QuantitativeValue yield\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Craft Project\"\n        yield={{\n          \"@type\": \"QuantitativeValue\",\n          value: 10,\n          unitText: \"pieces\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.yield).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 10,\n      unitText: \"pieces\",\n    });\n  });\n\n  it(\"handles QuantitativeValue yield without @type\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Craft Project\"\n        yield={{\n          value: 5,\n          unitText: \"items\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.yield[\"@type\"]).toBe(\"QuantitativeValue\");\n    expect(jsonData.yield.value).toBe(5);\n    expect(jsonData.yield.unitText).toBe(\"items\");\n  });\n\n  it(\"handles description\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"How to Change a Tire\"\n        description=\"Step-by-step instructions for changing a flat tire safely\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.description).toBe(\n      \"Step-by-step instructions for changing a flat tire safely\",\n    );\n  });\n\n  it(\"handles string image\", () => {\n    const { container } = render(\n      <HowToJsonLd name=\"How to Guide\" image=\"https://example.com/howto.jpg\" />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.image).toBe(\"https://example.com/howto.jpg\");\n  });\n\n  it(\"handles ImageObject\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"How to Guide\"\n        image={{\n          \"@type\": \"ImageObject\",\n          url: \"https://example.com/howto.jpg\",\n          width: 1200,\n          height: 800,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.image).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/howto.jpg\",\n      width: 1200,\n      height: 800,\n    });\n  });\n\n  it(\"handles ImageObject without @type\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"How to Guide\"\n        image={{\n          url: \"https://example.com/howto.jpg\",\n          width: 800,\n          height: 600,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.image[\"@type\"]).toBe(\"ImageObject\");\n    expect(jsonData.image.url).toBe(\"https://example.com/howto.jpg\");\n  });\n\n  it(\"handles video\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"How to Guide\"\n        video={{\n          \"@type\": \"VideoObject\",\n          name: \"How to Video\",\n          description: \"A video tutorial\",\n          thumbnailUrl: \"https://example.com/thumb.jpg\",\n          uploadDate: \"2024-01-01T08:00:00+00:00\",\n          contentUrl: \"https://example.com/video.mp4\",\n          duration: \"PT5M\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.video).toEqual({\n      \"@type\": \"VideoObject\",\n      name: \"How to Video\",\n      description: \"A video tutorial\",\n      thumbnailUrl: \"https://example.com/thumb.jpg\",\n      uploadDate: \"2024-01-01T08:00:00+00:00\",\n      contentUrl: \"https://example.com/video.mp4\",\n      duration: \"PT5M\",\n    });\n  });\n\n  it(\"handles video without @type\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"How to Guide\"\n        video={{\n          name: \"Tutorial Video\",\n          description: \"Step by step\",\n          thumbnailUrl: \"https://example.com/thumb.jpg\",\n          uploadDate: \"2024-01-01T08:00:00+00:00\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.video[\"@type\"]).toBe(\"VideoObject\");\n  });\n\n  it(\"handles single step\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Simple Guide\"\n        step={{\n          \"@type\": \"HowToStep\",\n          text: \"Just do this one thing\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.step).toEqual({\n      \"@type\": \"HowToStep\",\n      text: \"Just do this one thing\",\n    });\n  });\n\n  it(\"handles single supply\", () => {\n    const { container } = render(\n      <HowToJsonLd name=\"Simple Guide\" supply=\"Just one item\" />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.supply).toEqual({\n      \"@type\": \"HowToSupply\",\n      name: \"Just one item\",\n    });\n  });\n\n  it(\"handles single tool\", () => {\n    const { container } = render(\n      <HowToJsonLd name=\"Simple Guide\" tool=\"Just one tool\" />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.tool).toEqual({\n      \"@type\": \"HowToTool\",\n      name: \"Just one tool\",\n    });\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Complete HowTo Guide\"\n        description=\"A comprehensive guide with all properties\"\n        image=\"https://example.com/howto.jpg\"\n        estimatedCost=\"$50\"\n        prepTime=\"PT10M\"\n        performTime=\"PT30M\"\n        totalTime=\"PT40M\"\n        yield=\"1 completed project\"\n        tool={[\"Hammer\", \"Screwdriver\"]}\n        supply={[\"Nails\", \"Screws\"]}\n        step={[\n          {\n            \"@type\": \"HowToStep\",\n            name: \"Step 1\",\n            text: \"First, do this\",\n          },\n          {\n            \"@type\": \"HowToStep\",\n            name: \"Step 2\",\n            text: \"Then, do that\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"HowTo\");\n    expect(jsonData.name).toBe(\"Complete HowTo Guide\");\n    expect(jsonData.description).toBe(\n      \"A comprehensive guide with all properties\",\n    );\n    expect(jsonData.image).toBe(\"https://example.com/howto.jpg\");\n    expect(jsonData.estimatedCost).toBe(\"$50\");\n    expect(jsonData.prepTime).toBe(\"PT10M\");\n    expect(jsonData.performTime).toBe(\"PT30M\");\n    expect(jsonData.totalTime).toBe(\"PT40M\");\n    expect(jsonData.yield).toBe(\"1 completed project\");\n    expect(jsonData.tool).toHaveLength(2);\n    expect(jsonData.supply).toHaveLength(2);\n    expect(jsonData.step).toHaveLength(2);\n  });\n\n  it(\"uses custom scriptId when provided\", () => {\n    const { container } = render(\n      <HowToJsonLd name=\"Test HowTo\" scriptId=\"custom-howto-id\" />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script!.getAttribute(\"id\")).toBe(\"custom-howto-id\");\n    expect(script!.getAttribute(\"data-testid\")).toBe(\"custom-howto-id\");\n  });\n\n  it(\"uses custom scriptKey when provided\", () => {\n    const { container } = render(\n      <HowToJsonLd name=\"Test HowTo\" scriptKey=\"custom-key\" />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n  });\n\n  it(\"uses default scriptKey when not provided\", () => {\n    const { container } = render(<HowToJsonLd name=\"Test HowTo\" />);\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n  });\n\n  it(\"handles steps with directions and tips\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Guide with Tips\"\n        step={[\n          {\n            \"@type\": \"HowToStep\",\n            position: 1,\n            itemListElement: [\n              {\n                \"@type\": \"HowToDirection\",\n                position: 1,\n                text: \"Do this first\",\n              },\n              {\n                \"@type\": \"HowToTip\",\n                position: 2,\n                text: \"Here's a helpful tip\",\n              },\n            ],\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.step[0].itemListElement).toHaveLength(2);\n    expect(jsonData.step[0].itemListElement[0][\"@type\"]).toBe(\"HowToDirection\");\n    expect(jsonData.step[0].itemListElement[1][\"@type\"]).toBe(\"HowToTip\");\n  });\n\n  it(\"handles directions with media\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Visual Guide\"\n        step={[\n          {\n            \"@type\": \"HowToStep\",\n            itemListElement: [\n              {\n                \"@type\": \"HowToDirection\",\n                text: \"Position the jack\",\n                beforeMedia: \"https://example.com/before.jpg\",\n                afterMedia: \"https://example.com/after.jpg\",\n                duringMedia: \"https://example.com/during.jpg\",\n              },\n            ],\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    const direction = jsonData.step[0].itemListElement[0];\n    expect(direction.beforeMedia).toBe(\"https://example.com/before.jpg\");\n    expect(direction.afterMedia).toBe(\"https://example.com/after.jpg\");\n    expect(direction.duringMedia).toBe(\"https://example.com/during.jpg\");\n  });\n\n  it(\"handles HowToSupply with requiredQuantity as QuantitativeValue\", () => {\n    const { container } = render(\n      <HowToJsonLd\n        name=\"Project\"\n        supply={[\n          {\n            name: \"Screws\",\n            requiredQuantity: {\n              value: 10,\n              unitText: \"pieces\",\n            },\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.supply[0].requiredQuantity[\"@type\"]).toBe(\n      \"QuantitativeValue\",\n    );\n    expect(jsonData.supply[0].requiredQuantity.value).toBe(10);\n    expect(jsonData.supply[0].requiredQuantity.unitText).toBe(\"pieces\");\n  });\n});\n"
  },
  {
    "path": "src/components/HowToJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { HowToJsonLdProps } from \"~/types/howto.types\";\nimport {\n  processImage,\n  processVideo,\n  processStep,\n  processHowToSupply,\n  processHowToTool,\n  processEstimatedCost,\n  processHowToYield,\n} from \"~/utils/processors\";\n\nexport default function HowToJsonLd({\n  scriptId,\n  scriptKey,\n  name,\n  description,\n  image,\n  estimatedCost,\n  performTime,\n  prepTime,\n  totalTime,\n  step,\n  supply,\n  tool,\n  yield: yieldValue,\n  video,\n}: HowToJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"HowTo\",\n    name,\n    ...(description && { description }),\n    ...(image && {\n      image: Array.isArray(image)\n        ? image.map(processImage)\n        : processImage(image),\n    }),\n    ...(estimatedCost && {\n      estimatedCost: processEstimatedCost(estimatedCost),\n    }),\n    ...(performTime && { performTime }),\n    ...(prepTime && { prepTime }),\n    ...(totalTime && { totalTime }),\n    ...(step && {\n      step: Array.isArray(step) ? step.map(processStep) : processStep(step),\n    }),\n    ...(supply && {\n      supply: Array.isArray(supply)\n        ? supply.map(processHowToSupply)\n        : processHowToSupply(supply),\n    }),\n    ...(tool && {\n      tool: Array.isArray(tool)\n        ? tool.map(processHowToTool)\n        : processHowToTool(tool),\n    }),\n    ...(yieldValue && { yield: processHowToYield(yieldValue) }),\n    ...(video && { video: processVideo(video) }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"howto-jsonld\"}\n    />\n  );\n}\n\nexport type { HowToJsonLdProps };\n"
  },
  {
    "path": "src/components/ImageJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport ImageJsonLd from \"./ImageJsonLd\";\n\ndescribe(\"ImageJsonLd\", () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n  });\n\n  it(\"renders basic Image with minimal props\", () => {\n    const { container } = render(\n      <ImageJsonLd\n        contentUrl=\"https://example.com/photos/black-labrador.jpg\"\n        creator=\"Brixton Brownstone\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"ImageObject\",\n      contentUrl: \"https://example.com/photos/black-labrador.jpg\",\n      creator: {\n        \"@type\": \"Person\",\n        name: \"Brixton Brownstone\",\n      },\n    });\n  });\n\n  it(\"handles Organization creator with logo\", () => {\n    const { container } = render(\n      <ImageJsonLd\n        contentUrl=\"https://example.com/photos/product.jpg\"\n        creator={{\n          name: \"ACME Corp\",\n          logo: \"https://example.com/logo.jpg\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.creator).toEqual({\n      \"@type\": \"Organization\",\n      name: \"ACME Corp\",\n      logo: \"https://example.com/logo.jpg\",\n    });\n  });\n\n  it(\"handles multiple creators\", () => {\n    const { container } = render(\n      <ImageJsonLd\n        contentUrl=\"https://example.com/photos/collaboration.jpg\"\n        creator={[\"John Doe\", { name: \"Jane Smith\", url: \"https://jane.com\" }]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.creator).toEqual([\n      {\n        \"@type\": \"Person\",\n        name: \"John Doe\",\n      },\n      {\n        \"@type\": \"Person\",\n        name: \"Jane Smith\",\n        url: \"https://jane.com\",\n      },\n    ]);\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <ImageJsonLd\n        contentUrl=\"https://example.com/photos/licensed-photo.jpg\"\n        creator=\"Professional Photographer\"\n        creditText=\"PhotoLab Studios\"\n        copyrightNotice=\"© 2024 PhotoLab\"\n        license=\"https://creativecommons.org/licenses/by-nc/4.0/\"\n        acquireLicensePage=\"https://example.com/licensing\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"ImageObject\",\n      contentUrl: \"https://example.com/photos/licensed-photo.jpg\",\n      creator: {\n        \"@type\": \"Person\",\n        name: \"Professional Photographer\",\n      },\n      creditText: \"PhotoLab Studios\",\n      copyrightNotice: \"© 2024 PhotoLab\",\n      license: \"https://creativecommons.org/licenses/by-nc/4.0/\",\n      acquireLicensePage: \"https://example.com/licensing\",\n    });\n  });\n\n  it(\"renders multiple images\", () => {\n    const { container } = render(\n      <ImageJsonLd\n        images={[\n          {\n            contentUrl: \"https://example.com/photos/photo1.jpg\",\n            creator: \"Photographer 1\",\n            license: \"https://example.com/license1\",\n          },\n          {\n            contentUrl: \"https://example.com/photos/photo2.jpg\",\n            creditText: \"Studio 2\",\n            copyrightNotice: \"© 2024 Studio 2\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@graph\": [\n        {\n          \"@type\": \"ImageObject\",\n          contentUrl: \"https://example.com/photos/photo1.jpg\",\n          creator: {\n            \"@type\": \"Person\",\n            name: \"Photographer 1\",\n          },\n          license: \"https://example.com/license1\",\n        },\n        {\n          \"@type\": \"ImageObject\",\n          contentUrl: \"https://example.com/photos/photo2.jpg\",\n          creditText: \"Studio 2\",\n          copyrightNotice: \"© 2024 Studio 2\",\n        },\n      ],\n    });\n  });\n\n  it(\"warns when contentUrl is missing\", () => {\n    const consoleWarnSpy = vi\n      .spyOn(console, \"warn\")\n      .mockImplementation(() => {});\n\n    render(<ImageJsonLd contentUrl=\"\" creator=\"Test Creator\" />);\n\n    expect(consoleWarnSpy).toHaveBeenCalledWith(\n      \"ImageJsonLd: contentUrl and at least one of creator, creditText, copyrightNotice, or license is required\",\n    );\n  });\n\n  it(\"warns when no required metadata fields are provided\", () => {\n    const consoleWarnSpy = vi\n      .spyOn(console, \"warn\")\n      .mockImplementation(() => {});\n\n    render(<ImageJsonLd contentUrl=\"https://example.com/photo.jpg\" />);\n\n    expect(consoleWarnSpy).toHaveBeenCalledWith(\n      \"ImageJsonLd: contentUrl and at least one of creator, creditText, copyrightNotice, or license is required\",\n    );\n  });\n\n  it(\"uses custom scriptId when provided\", () => {\n    const { container } = render(\n      <ImageJsonLd\n        contentUrl=\"https://example.com/photo.jpg\"\n        creator=\"Test Creator\"\n        scriptId=\"custom-image-id\"\n      />,\n    );\n\n    const script = container.querySelector(\"#custom-image-id\");\n    expect(script).toBeTruthy();\n    expect(script?.getAttribute(\"type\")).toBe(\"application/ld+json\");\n  });\n\n  it(\"uses custom scriptKey when provided\", () => {\n    const { container } = render(\n      <ImageJsonLd\n        contentUrl=\"https://example.com/photo.jpg\"\n        creator=\"Test Creator\"\n        scriptKey=\"custom-key\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n    // When no scriptId is provided, scriptKey is used as the id\n    expect(script?.getAttribute(\"id\")).toBe(\"custom-key\");\n  });\n\n  it(\"handles Person creator with @type already specified\", () => {\n    const { container } = render(\n      <ImageJsonLd\n        contentUrl=\"https://example.com/photo.jpg\"\n        creator={{\n          \"@type\": \"Person\",\n          name: \"John Doe\",\n          familyName: \"Doe\",\n          givenName: \"John\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.creator).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n      familyName: \"Doe\",\n      givenName: \"John\",\n    });\n  });\n\n  it(\"handles Organization creator with @type already specified\", () => {\n    const { container } = render(\n      <ImageJsonLd\n        contentUrl=\"https://example.com/photo.jpg\"\n        creator={{\n          \"@type\": \"Organization\",\n          name: \"ACME Corp\",\n          sameAs: [\"https://twitter.com/acme\", \"https://facebook.com/acme\"],\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.creator).toEqual({\n      \"@type\": \"Organization\",\n      name: \"ACME Corp\",\n      sameAs: [\"https://twitter.com/acme\", \"https://facebook.com/acme\"],\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/ImageJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { ImageJsonLdProps, ImageMetadata } from \"~/types/image.types\";\nimport { processAuthor } from \"~/utils/processors\";\n\nexport default function ImageJsonLd({\n  scriptId,\n  scriptKey,\n  ...props\n}: ImageJsonLdProps) {\n  // Helper function to process a single image\n  const processImage = (image: Omit<ImageMetadata, \"@type\">) => {\n    // Validate required fields\n    const hasRequiredMetadata =\n      image.creator ||\n      image.creditText ||\n      image.copyrightNotice ||\n      image.license;\n\n    if (!image.contentUrl || !hasRequiredMetadata) {\n      console.warn(\n        \"ImageJsonLd: contentUrl and at least one of creator, creditText, copyrightNotice, or license is required\",\n      );\n    }\n\n    return {\n      \"@type\": \"ImageObject\",\n      contentUrl: image.contentUrl,\n      ...(image.creator && {\n        creator: Array.isArray(image.creator)\n          ? image.creator.map(processAuthor)\n          : processAuthor(image.creator),\n      }),\n      ...(image.creditText && { creditText: image.creditText }),\n      ...(image.copyrightNotice && { copyrightNotice: image.copyrightNotice }),\n      ...(image.license && { license: image.license }),\n      ...(image.acquireLicensePage && {\n        acquireLicensePage: image.acquireLicensePage,\n      }),\n    };\n  };\n\n  // Determine if we have multiple images\n  const hasMultipleImages = \"images\" in props && Array.isArray(props.images);\n\n  const data = hasMultipleImages\n    ? props.images.map(processImage)\n    : processImage(props as Omit<ImageMetadata, \"@type\">);\n\n  return (\n    <JsonLdScript\n      data={{\n        \"@context\": \"https://schema.org\",\n        ...(Array.isArray(data) ? {} : data),\n        ...(Array.isArray(data) && { \"@graph\": data }),\n      }}\n      id={scriptId}\n      scriptKey={scriptKey || \"image-jsonld\"}\n    />\n  );\n}\n\nexport type { ImageJsonLdProps };\n"
  },
  {
    "path": "src/components/JobPostingJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport JobPostingJsonLd from \"./JobPostingJsonLd\";\n\ndescribe(\"JobPostingJsonLd\", () => {\n  it(\"renders basic JobPosting with minimal props\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>We are looking for a software engineer to join our team.</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"JobPosting\",\n      title: \"Software Engineer\",\n      description:\n        \"<p>We are looking for a software engineer to join our team.</p>\",\n      datePosted: \"2024-01-18\",\n      hiringOrganization: {\n        \"@type\": \"Organization\",\n        name: \"Google\",\n      },\n    });\n  });\n\n  it(\"handles hiringOrganization as an object\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization={{\n          name: \"Google\",\n          sameAs: \"https://www.google.com\",\n          logo: \"https://www.google.com/logo.png\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.hiringOrganization).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Google\",\n      sameAs: \"https://www.google.com\",\n      logo: \"https://www.google.com/logo.png\",\n    });\n  });\n\n  it(\"handles string jobLocation\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        jobLocation=\"Mountain View, CA\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.jobLocation).toEqual({\n      \"@type\": \"Place\",\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"Mountain View, CA\",\n      },\n    });\n  });\n\n  it(\"handles object jobLocation with address\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        jobLocation={{\n          address: {\n            streetAddress: \"1600 Amphitheatre Pkwy\",\n            addressLocality: \"Mountain View\",\n            addressRegion: \"CA\",\n            postalCode: \"94043\",\n            addressCountry: \"US\",\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.jobLocation).toEqual({\n      \"@type\": \"Place\",\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"1600 Amphitheatre Pkwy\",\n        addressLocality: \"Mountain View\",\n        addressRegion: \"CA\",\n        postalCode: \"94043\",\n        addressCountry: \"US\",\n      },\n    });\n  });\n\n  it(\"handles multiple job locations\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        jobLocation={[\"Mountain View, CA\", \"New York, NY\"]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.jobLocation).toEqual([\n      {\n        \"@type\": \"Place\",\n        address: {\n          \"@type\": \"PostalAddress\",\n          streetAddress: \"Mountain View, CA\",\n        },\n      },\n      {\n        \"@type\": \"Place\",\n        address: {\n          \"@type\": \"PostalAddress\",\n          streetAddress: \"New York, NY\",\n        },\n      },\n    ]);\n  });\n\n  it(\"handles remote job with jobLocationType\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Remote position!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        jobLocationType=\"TELECOMMUTE\"\n        applicantLocationRequirements={{ name: \"USA\" }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.jobLocationType).toBe(\"TELECOMMUTE\");\n    expect(jsonData.applicantLocationRequirements).toEqual({\n      \"@type\": \"Country\",\n      name: \"USA\",\n    });\n  });\n\n  it(\"handles state applicant location requirements\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        applicantLocationRequirements={[\n          { name: \"Michigan, USA\" },\n          { name: \"Texas, USA\" },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.applicantLocationRequirements).toEqual([\n      {\n        \"@type\": \"State\",\n        name: \"Michigan, USA\",\n      },\n      {\n        \"@type\": \"State\",\n        name: \"Texas, USA\",\n      },\n    ]);\n  });\n\n  it(\"handles baseSalary with value\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        baseSalary={{\n          currency: \"USD\",\n          value: {\n            value: 40.0,\n            unitText: \"HOUR\",\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.baseSalary).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      currency: \"USD\",\n      value: {\n        \"@type\": \"QuantitativeValue\",\n        value: 40.0,\n        unitText: \"HOUR\",\n      },\n    });\n  });\n\n  it(\"handles baseSalary with range\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        baseSalary={{\n          currency: \"USD\",\n          value: {\n            minValue: 40.0,\n            maxValue: 50.0,\n            unitText: \"HOUR\",\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.baseSalary).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      currency: \"USD\",\n      value: {\n        \"@type\": \"QuantitativeValue\",\n        minValue: 40.0,\n        maxValue: 50.0,\n        unitText: \"HOUR\",\n      },\n    });\n  });\n\n  it(\"handles employmentType as single value\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        employmentType=\"FULL_TIME\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.employmentType).toBe(\"FULL_TIME\");\n  });\n\n  it(\"handles employmentType as array\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        employmentType={[\"FULL_TIME\", \"CONTRACTOR\"]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.employmentType).toEqual([\"FULL_TIME\", \"CONTRACTOR\"]);\n  });\n\n  it(\"handles identifier as string\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        identifier=\"1234567\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.identifier).toEqual({\n      \"@type\": \"PropertyValue\",\n      value: \"1234567\",\n    });\n  });\n\n  it(\"handles identifier as object\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        identifier={{\n          name: \"Google\",\n          value: \"1234567\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.identifier).toEqual({\n      \"@type\": \"PropertyValue\",\n      name: \"Google\",\n      value: \"1234567\",\n    });\n  });\n\n  it(\"handles educationRequirements as string\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        educationRequirements=\"bachelor degree\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.educationRequirements).toBe(\"bachelor degree\");\n  });\n\n  it(\"handles educationRequirements as object\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        educationRequirements={{\n          credentialCategory: \"bachelor degree\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.educationRequirements).toEqual({\n      \"@type\": \"EducationalOccupationalCredential\",\n      credentialCategory: \"bachelor degree\",\n    });\n  });\n\n  it(\"handles multiple educationRequirements\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        educationRequirements={[\n          { credentialCategory: \"bachelor degree\" },\n          { credentialCategory: \"postgraduate degree\" },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.educationRequirements).toEqual([\n      {\n        \"@type\": \"EducationalOccupationalCredential\",\n        credentialCategory: \"bachelor degree\",\n      },\n      {\n        \"@type\": \"EducationalOccupationalCredential\",\n        credentialCategory: \"postgraduate degree\",\n      },\n    ]);\n  });\n\n  it(\"handles experienceRequirements as string\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        experienceRequirements=\"3+ years of experience\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.experienceRequirements).toBe(\"3+ years of experience\");\n  });\n\n  it(\"handles experienceRequirements as object\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        experienceRequirements={{\n          monthsOfExperience: 36,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.experienceRequirements).toEqual({\n      \"@type\": \"OccupationalExperienceRequirements\",\n      monthsOfExperience: 36,\n    });\n  });\n\n  it(\"handles boolean directApply as false\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        directApply={false}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.directApply).toBe(false);\n  });\n\n  it(\"handles experienceInPlaceOfEducation\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n        educationRequirements={{ credentialCategory: \"bachelor degree\" }}\n        experienceRequirements={{ monthsOfExperience: 36 }}\n        experienceInPlaceOfEducation={true}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.experienceInPlaceOfEducation).toBe(true);\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization={{\n          name: \"Google\",\n          sameAs: \"https://www.google.com\",\n          logo: \"https://www.google.com/logo.png\",\n        }}\n        jobLocation={{\n          address: {\n            streetAddress: \"1600 Amphitheatre Pkwy\",\n            addressLocality: \"Mountain View\",\n            addressRegion: \"CA\",\n            postalCode: \"94043\",\n            addressCountry: \"US\",\n          },\n        }}\n        url=\"https://careers.google.com/jobs/123\"\n        validThrough=\"2024-03-18T00:00\"\n        employmentType=\"CONTRACTOR\"\n        identifier={{\n          name: \"Google\",\n          value: \"1234567\",\n        }}\n        baseSalary={{\n          currency: \"USD\",\n          value: {\n            value: 40.0,\n            unitText: \"HOUR\",\n          },\n        }}\n        directApply={true}\n        educationRequirements={{\n          credentialCategory: \"bachelor degree\",\n        }}\n        experienceRequirements={{\n          monthsOfExperience: 36,\n        }}\n        experienceInPlaceOfEducation={true}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"JobPosting\",\n      title: \"Software Engineer\",\n      description: \"<p>Join our team!</p>\",\n      datePosted: \"2024-01-18\",\n      hiringOrganization: {\n        \"@type\": \"Organization\",\n        name: \"Google\",\n        sameAs: \"https://www.google.com\",\n        logo: \"https://www.google.com/logo.png\",\n      },\n      jobLocation: {\n        \"@type\": \"Place\",\n        address: {\n          \"@type\": \"PostalAddress\",\n          streetAddress: \"1600 Amphitheatre Pkwy\",\n          addressLocality: \"Mountain View\",\n          addressRegion: \"CA\",\n          postalCode: \"94043\",\n          addressCountry: \"US\",\n        },\n      },\n      url: \"https://careers.google.com/jobs/123\",\n      validThrough: \"2024-03-18T00:00\",\n      employmentType: \"CONTRACTOR\",\n      identifier: {\n        \"@type\": \"PropertyValue\",\n        name: \"Google\",\n        value: \"1234567\",\n      },\n      baseSalary: {\n        \"@type\": \"MonetaryAmount\",\n        currency: \"USD\",\n        value: {\n          \"@type\": \"QuantitativeValue\",\n          value: 40.0,\n          unitText: \"HOUR\",\n        },\n      },\n      directApply: true,\n      educationRequirements: {\n        \"@type\": \"EducationalOccupationalCredential\",\n        credentialCategory: \"bachelor degree\",\n      },\n      experienceRequirements: {\n        \"@type\": \"OccupationalExperienceRequirements\",\n        monthsOfExperience: 36,\n      },\n      experienceInPlaceOfEducation: true,\n    });\n  });\n\n  it(\"uses custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <JobPostingJsonLd\n        scriptId=\"custom-id\"\n        scriptKey=\"custom-key\"\n        title=\"Software Engineer\"\n        description=\"<p>Join our team!</p>\"\n        datePosted=\"2024-01-18\"\n        hiringOrganization=\"Google\"\n      />,\n    );\n\n    const script = container.querySelector('script[id=\"custom-id\"]');\n    expect(script).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "src/components/JobPostingJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { JobPostingJsonLdProps } from \"~/types/jobposting.types\";\nimport {\n  processHiringOrganization,\n  processJobLocation,\n  processMonetaryAmount,\n  processJobPropertyValue,\n  processApplicantLocationRequirements,\n  processEducationRequirements,\n  processExperienceRequirements,\n} from \"~/utils/processors\";\n\nexport default function JobPostingJsonLd({\n  scriptId,\n  scriptKey,\n  title,\n  description,\n  datePosted,\n  hiringOrganization,\n  jobLocation,\n  url,\n  validThrough,\n  employmentType,\n  identifier,\n  baseSalary,\n  applicantLocationRequirements,\n  jobLocationType,\n  directApply,\n  educationRequirements,\n  experienceRequirements,\n  experienceInPlaceOfEducation,\n}: JobPostingJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"JobPosting\",\n    title,\n    description,\n    datePosted,\n    hiringOrganization: processHiringOrganization(hiringOrganization),\n    ...(jobLocation && {\n      jobLocation: Array.isArray(jobLocation)\n        ? jobLocation.map(processJobLocation)\n        : processJobLocation(jobLocation),\n    }),\n    ...(url && { url }),\n    ...(validThrough && { validThrough }),\n    ...(employmentType && {\n      employmentType: Array.isArray(employmentType)\n        ? employmentType\n        : employmentType,\n    }),\n    ...(identifier && {\n      identifier: processJobPropertyValue(identifier),\n    }),\n    ...(baseSalary && {\n      baseSalary: processMonetaryAmount(baseSalary),\n    }),\n    ...(applicantLocationRequirements && {\n      applicantLocationRequirements: Array.isArray(\n        applicantLocationRequirements,\n      )\n        ? applicantLocationRequirements.map(\n            processApplicantLocationRequirements,\n          )\n        : processApplicantLocationRequirements(applicantLocationRequirements),\n    }),\n    ...(jobLocationType && { jobLocationType }),\n    ...(directApply !== undefined && { directApply }),\n    ...(educationRequirements && {\n      educationRequirements: Array.isArray(educationRequirements)\n        ? educationRequirements.map(processEducationRequirements)\n        : processEducationRequirements(educationRequirements),\n    }),\n    ...(experienceRequirements && {\n      experienceRequirements: processExperienceRequirements(\n        experienceRequirements,\n      ),\n    }),\n    ...(experienceInPlaceOfEducation !== undefined && {\n      experienceInPlaceOfEducation,\n    }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"jobposting-jsonld\"}\n    />\n  );\n}\n\nexport type { JobPostingJsonLdProps };\n"
  },
  {
    "path": "src/components/LocalBusinessJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport LocalBusinessJsonLd from \"./LocalBusinessJsonLd\";\n\ndescribe(\"LocalBusinessJsonLd\", () => {\n  it(\"renders basic LocalBusiness with minimal props\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Dave's Steak House\"\n        address=\"148 W 51st St, New York, NY 10019\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"LocalBusiness\",\n      name: \"Dave's Steak House\",\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"148 W 51st St, New York, NY 10019\",\n      },\n    });\n  });\n\n  it(\"renders Restaurant type when specified\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        type=\"Restaurant\"\n        name=\"Dave's Steak House\"\n        address={{\n          \"@type\": \"PostalAddress\",\n          streetAddress: \"148 W 51st St\",\n          addressLocality: \"New York\",\n          addressRegion: \"NY\",\n          postalCode: \"10019\",\n          addressCountry: \"US\",\n        }}\n        servesCuisine=\"American\"\n        menu=\"https://example.com/menu\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toBe(\"Restaurant\");\n    expect(jsonData.servesCuisine).toEqual([\"American\"]);\n    expect(jsonData.menu).toBe(\"https://example.com/menu\");\n  });\n\n  it(\"handles multiple types as array\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        type={[\"Restaurant\", \"BarOrPub\"]}\n        name=\"Dave's Restaurant & Bar\"\n        address=\"123 Main St\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toEqual([\"Restaurant\", \"BarOrPub\"]);\n  });\n\n  it(\"handles multiple addresses\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Multi-location Business\"\n        address={[\n          \"123 Main St, City A\",\n          {\n            \"@type\": \"PostalAddress\",\n            streetAddress: \"456 Oak Ave\",\n            addressLocality: \"City B\",\n            addressRegion: \"State\",\n            postalCode: \"12345\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.address).toEqual([\n      {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"123 Main St, City A\",\n      },\n      {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"456 Oak Ave\",\n        addressLocality: \"City B\",\n        addressRegion: \"State\",\n        postalCode: \"12345\",\n      },\n    ]);\n  });\n\n  it(\"handles complex opening hours\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Test Business\"\n        address=\"123 Main St\"\n        openingHoursSpecification={[\n          {\n            \"@type\": \"OpeningHoursSpecification\",\n            dayOfWeek: [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"],\n            opens: \"09:00\",\n            closes: \"21:00\",\n          },\n          {\n            \"@type\": \"OpeningHoursSpecification\",\n            dayOfWeek: \"Saturday\",\n            opens: \"10:00\",\n            closes: \"23:00\",\n          },\n          {\n            \"@type\": \"OpeningHoursSpecification\",\n            dayOfWeek: \"Sunday\",\n            opens: \"00:00\",\n            closes: \"00:00\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.openingHoursSpecification).toHaveLength(3);\n    expect(jsonData.openingHoursSpecification[0].dayOfWeek).toEqual([\n      \"Monday\",\n      \"Tuesday\",\n      \"Wednesday\",\n      \"Thursday\",\n      \"Friday\",\n    ]);\n  });\n\n  it(\"handles seasonal hours with validFrom and validThrough\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Seasonal Business\"\n        address=\"123 Beach Rd\"\n        openingHoursSpecification={{\n          \"@type\": \"OpeningHoursSpecification\",\n          dayOfWeek: [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"],\n          opens: \"10:00\",\n          closes: \"18:00\",\n          validFrom: \"2024-06-01\",\n          validThrough: \"2024-09-30\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.openingHoursSpecification.validFrom).toBe(\"2024-06-01\");\n    expect(jsonData.openingHoursSpecification.validThrough).toBe(\"2024-09-30\");\n  });\n\n  it(\"handles departments\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        type=\"Store\"\n        name=\"Dave's Department Store\"\n        address=\"1600 Saratoga Ave, San Jose, CA 95129\"\n        telephone=\"+14088717984\"\n        department={[\n          {\n            type: \"Pharmacy\",\n            name: \"Dave's Pharmacy\",\n            address: \"1600 Saratoga Ave, San Jose, CA 95129\",\n            telephone: \"+14088719385\",\n            openingHoursSpecification: {\n              \"@type\": \"OpeningHoursSpecification\",\n              dayOfWeek: [\n                \"Monday\",\n                \"Tuesday\",\n                \"Wednesday\",\n                \"Thursday\",\n                \"Friday\",\n              ],\n              opens: \"09:00\",\n              closes: \"19:00\",\n            },\n          },\n          {\n            type: \"Bakery\",\n            name: \"Dave's Bakery\",\n            address: \"1600 Saratoga Ave, San Jose, CA 95129\",\n            telephone: \"+14088719386\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.department).toHaveLength(2);\n    expect(jsonData.department[0][\"@type\"]).toBe(\"Pharmacy\");\n    expect(jsonData.department[0].name).toBe(\"Dave's Pharmacy\");\n    expect(jsonData.department[1][\"@type\"]).toBe(\"Bakery\");\n  });\n\n  it(\"handles review and aggregateRating\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Reviewed Business\"\n        address=\"123 Main St\"\n        review={{\n          \"@type\": \"Review\",\n          reviewRating: {\n            \"@type\": \"Rating\",\n            ratingValue: 4,\n            bestRating: 5,\n          },\n          author: \"John Doe\",\n          reviewBody: \"Great service!\",\n          datePublished: \"2024-01-15\",\n        }}\n        aggregateRating={{\n          \"@type\": \"AggregateRating\",\n          ratingValue: 4.5,\n          ratingCount: 100,\n          reviewCount: 95,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.review.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n    });\n    expect(jsonData.review.reviewRating.ratingValue).toBe(4);\n    expect(jsonData.aggregateRating.ratingValue).toBe(4.5);\n  });\n\n  it(\"handles geo coordinates\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Geo Business\"\n        address=\"123 Main St\"\n        geo={{\n          \"@type\": \"GeoCoordinates\",\n          latitude: 40.761293,\n          longitude: -73.982294,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.geo).toEqual({\n      \"@type\": \"GeoCoordinates\",\n      latitude: 40.761293,\n      longitude: -73.982294,\n    });\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        type=\"Restaurant\"\n        name=\"Full Featured Restaurant\"\n        address=\"123 Main St\"\n        url=\"https://example.com\"\n        telephone=\"+12125551234\"\n        image={[\n          \"https://example.com/photos/1x1/photo.jpg\",\n          {\n            \"@type\": \"ImageObject\",\n            url: \"https://example.com/photos/4x3/photo.jpg\",\n            width: 400,\n            height: 300,\n          },\n        ]}\n        priceRange=\"$$$\"\n        servesCuisine={[\"Italian\", \"American\"]}\n        menu=\"https://example.com/menu\"\n        sameAs={[\n          \"https://facebook.com/restaurant\",\n          \"https://twitter.com/restaurant\",\n        ]}\n        branchOf={{\n          \"@type\": \"Organization\",\n          name: \"Parent Company\",\n        }}\n        currenciesAccepted=\"USD\"\n        paymentAccepted=\"Cash, Credit Card\"\n        areaServed={[\"New York\", \"New Jersey\"]}\n        email=\"info@example.com\"\n        faxNumber=\"+12125551235\"\n        slogan=\"Best food in town!\"\n        description=\"A great restaurant serving Italian and American cuisine\"\n        publicAccess={true}\n        smokingAllowed={false}\n        isAccessibleForFree={true}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.url).toBe(\"https://example.com\");\n    expect(jsonData.telephone).toBe(\"+12125551234\");\n    expect(jsonData.image).toHaveLength(2);\n    expect(jsonData.priceRange).toBe(\"$$$\");\n    expect(jsonData.servesCuisine).toEqual([\"Italian\", \"American\"]);\n    expect(jsonData.sameAs).toEqual([\n      \"https://facebook.com/restaurant\",\n      \"https://twitter.com/restaurant\",\n    ]);\n    expect(jsonData.publicAccess).toBe(true);\n    expect(jsonData.smokingAllowed).toBe(false);\n    expect(jsonData.isAccessibleForFree).toBe(true);\n  });\n\n  it(\"handles boolean values correctly\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Boolean Test Business\"\n        address=\"123 Main St\"\n        publicAccess={false}\n        smokingAllowed={false}\n        isAccessibleForFree={false}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.publicAccess).toBe(false);\n    expect(jsonData.smokingAllowed).toBe(false);\n    expect(jsonData.isAccessibleForFree).toBe(false);\n  });\n\n  it(\"uses custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Test Business\"\n        address=\"123 Main St\"\n        scriptId=\"custom-id\"\n        scriptKey=\"custom-key\"\n      />,\n    );\n\n    const script = container.querySelector(\"#custom-id\");\n    expect(script).toBeTruthy();\n    expect(script?.getAttribute(\"data-testid\")).toBe(\"custom-id\");\n  });\n\n  it(\"handles ImageObject without @type\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Business with Images\"\n        address=\"123 Main St\"\n        image={[\n          \"https://example.com/photo1.jpg\",\n          {\n            url: \"https://example.com/photo2.jpg\",\n            width: 800,\n            height: 600,\n          },\n          {\n            \"@type\": \"ImageObject\",\n            url: \"https://example.com/photo3.jpg\",\n            width: 1200,\n            height: 900,\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.image).toHaveLength(3);\n    expect(jsonData.image[0]).toBe(\"https://example.com/photo1.jpg\");\n    expect(jsonData.image[1]).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/photo2.jpg\",\n      width: 800,\n      height: 600,\n    });\n    expect(jsonData.image[2]).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/photo3.jpg\",\n      width: 1200,\n      height: 900,\n    });\n  });\n\n  it(\"handles AggregateRating without @type\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Rated Business\"\n        address=\"123 Main St\"\n        aggregateRating={{\n          ratingValue: 4.5,\n          ratingCount: 50,\n          bestRating: 5,\n          worstRating: 1,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.5,\n      ratingCount: 50,\n      bestRating: 5,\n      worstRating: 1,\n    });\n  });\n\n  it(\"handles single department with all properties\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        type=\"Store\"\n        name=\"Main Store\"\n        address=\"123 Main St\"\n        department={{\n          type: \"AutoPartsStore\",\n          name: \"Auto Parts Department\",\n          address: \"123 Main St, Section A\",\n          telephone: \"+12125551234\",\n          image: \"https://example.com/auto-parts.jpg\",\n          priceRange: \"$$\",\n          openingHoursSpecification: {\n            \"@type\": \"OpeningHoursSpecification\",\n            dayOfWeek: [\"Monday\", \"Friday\"],\n            opens: \"08:00\",\n            closes: \"18:00\",\n          },\n          geo: {\n            \"@type\": \"GeoCoordinates\",\n            latitude: 40.75,\n            longitude: -73.98,\n          },\n          review: {\n            \"@type\": \"Review\",\n            author: \"Jane Smith\",\n            reviewRating: {\n              \"@type\": \"Rating\",\n              ratingValue: 5,\n            },\n          },\n          aggregateRating: {\n            \"@type\": \"AggregateRating\",\n            ratingValue: 4.8,\n            reviewCount: 25,\n          },\n          sameAs: [\"https://facebook.com/autoparts\"],\n          branchOf: {\n            \"@type\": \"Organization\",\n            name: \"Auto Parts Inc\",\n          },\n          currenciesAccepted: \"USD\",\n          paymentAccepted: \"Cash, Credit Card\",\n          areaServed: [\"Downtown\", \"Midtown\"],\n          email: \"autoparts@example.com\",\n          faxNumber: \"+12125551235\",\n          slogan: \"Quality parts, great prices\",\n          description: \"Your one-stop shop for auto parts\",\n          publicAccess: true,\n          smokingAllowed: false,\n          isAccessibleForFree: true,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.department[\"@type\"]).toBe(\"AutoPartsStore\");\n    expect(jsonData.department.name).toBe(\"Auto Parts Department\");\n    expect(jsonData.department.currenciesAccepted).toBe(\"USD\");\n    expect(jsonData.department.paymentAccepted).toBe(\"Cash, Credit Card\");\n    expect(jsonData.department.areaServed).toEqual([\"Downtown\", \"Midtown\"]);\n    expect(jsonData.department.email).toBe(\"autoparts@example.com\");\n    expect(jsonData.department.faxNumber).toBe(\"+12125551235\");\n    expect(jsonData.department.slogan).toBe(\"Quality parts, great prices\");\n    expect(jsonData.department.description).toBe(\n      \"Your one-stop shop for auto parts\",\n    );\n    expect(jsonData.department.publicAccess).toBe(true);\n    expect(jsonData.department.smokingAllowed).toBe(false);\n    expect(jsonData.department.isAccessibleForFree).toBe(true);\n  });\n\n  it(\"handles array of reviews\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Multi-Review Business\"\n        address=\"123 Main St\"\n        review={[\n          {\n            author: \"John Doe\",\n            reviewRating: {\n              \"@type\": \"Rating\",\n              ratingValue: 5,\n            },\n            reviewBody: \"Excellent service!\",\n          },\n          {\n            author: {\n              \"@type\": \"Person\",\n              name: \"Jane Smith\",\n            },\n            reviewRating: {\n              \"@type\": \"Rating\",\n              ratingValue: 4,\n            },\n            reviewBody: \"Good experience\",\n          },\n          {\n            author: {\n              name: \"Bob Johnson\",\n              url: \"https://bobsblog.com\",\n            },\n            reviewRating: {\n              ratingValue: 4.5,\n            },\n            datePublished: \"2024-01-01\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.review).toHaveLength(3);\n    expect(jsonData.review[0].author).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n    });\n    expect(jsonData.review[1].author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Jane Smith\",\n    });\n    expect(jsonData.review[2].author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Bob Johnson\",\n      url: \"https://bobsblog.com\",\n    });\n    expect(jsonData.review[2].reviewRating).toEqual({\n      \"@type\": \"Rating\",\n      ratingValue: 4.5,\n    });\n  });\n\n  it(\"handles currenciesAccepted and paymentAccepted at root level\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Payment Test Business\"\n        address=\"123 Main St\"\n        currenciesAccepted=\"USD, EUR, GBP\"\n        paymentAccepted=\"Cash, Credit Card, PayPal\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.currenciesAccepted).toBe(\"USD, EUR, GBP\");\n    expect(jsonData.paymentAccepted).toBe(\"Cash, Credit Card, PayPal\");\n  });\n\n  it(\"handles areaServed as string\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Single Area Business\"\n        address=\"123 Main St\"\n        areaServed=\"Manhattan\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.areaServed).toEqual([\"Manhattan\"]);\n  });\n\n  it(\"handles sameAs as single string\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        name=\"Social Business\"\n        address=\"123 Main St\"\n        sameAs=\"https://facebook.com/business\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.sameAs).toEqual([\"https://facebook.com/business\"]);\n  });\n\n  it(\"handles servesCuisine as single string\", () => {\n    const { container } = render(\n      <LocalBusinessJsonLd\n        type=\"Restaurant\"\n        name=\"Italian Restaurant\"\n        address=\"123 Main St\"\n        servesCuisine=\"Italian\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.servesCuisine).toEqual([\"Italian\"]);\n  });\n});\n"
  },
  {
    "path": "src/components/LocalBusinessJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { LocalBusinessJsonLdProps } from \"~/types/localbusiness.types\";\nimport {\n  processAddress,\n  processImage,\n  processGeo,\n  processOpeningHours,\n  processReview,\n  processAggregateRating,\n} from \"~/utils/processors\";\n\nfunction processDepartment(\n  department: LocalBusinessJsonLdProps,\n): Record<string, unknown> {\n  return {\n    \"@type\": department.type || \"LocalBusiness\",\n    ...(department.name && { name: department.name }),\n    ...(department.address && {\n      address: Array.isArray(department.address)\n        ? department.address.map(processAddress)\n        : processAddress(department.address),\n    }),\n    ...(department.url && { url: department.url }),\n    ...(department.telephone && { telephone: department.telephone }),\n    ...(department.image && {\n      image: Array.isArray(department.image)\n        ? department.image.map(processImage)\n        : processImage(department.image),\n    }),\n    ...(department.priceRange && { priceRange: department.priceRange }),\n    ...(department.geo && { geo: processGeo(department.geo) }),\n    ...(department.openingHoursSpecification && {\n      openingHoursSpecification: Array.isArray(\n        department.openingHoursSpecification,\n      )\n        ? department.openingHoursSpecification.map(processOpeningHours)\n        : processOpeningHours(department.openingHoursSpecification),\n    }),\n    ...(department.review && {\n      review: Array.isArray(department.review)\n        ? department.review.map(processReview)\n        : processReview(department.review),\n    }),\n    ...(department.aggregateRating && {\n      aggregateRating: processAggregateRating(department.aggregateRating),\n    }),\n    ...(department.menu && { menu: department.menu }),\n    ...(department.servesCuisine && {\n      servesCuisine: Array.isArray(department.servesCuisine)\n        ? department.servesCuisine\n        : [department.servesCuisine],\n    }),\n    ...(department.sameAs && {\n      sameAs: Array.isArray(department.sameAs)\n        ? department.sameAs\n        : [department.sameAs],\n    }),\n    ...(department.branchOf && { branchOf: department.branchOf }),\n    ...(department.currenciesAccepted && {\n      currenciesAccepted: department.currenciesAccepted,\n    }),\n    ...(department.paymentAccepted && {\n      paymentAccepted: department.paymentAccepted,\n    }),\n    ...(department.areaServed && {\n      areaServed: Array.isArray(department.areaServed)\n        ? department.areaServed\n        : [department.areaServed],\n    }),\n    ...(department.email && { email: department.email }),\n    ...(department.faxNumber && { faxNumber: department.faxNumber }),\n    ...(department.slogan && { slogan: department.slogan }),\n    ...(department.description && { description: department.description }),\n    ...(department.publicAccess !== undefined && {\n      publicAccess: department.publicAccess,\n    }),\n    ...(department.smokingAllowed !== undefined && {\n      smokingAllowed: department.smokingAllowed,\n    }),\n    ...(department.isAccessibleForFree !== undefined && {\n      isAccessibleForFree: department.isAccessibleForFree,\n    }),\n  };\n}\n\nexport default function LocalBusinessJsonLd({\n  type = \"LocalBusiness\",\n  scriptId,\n  scriptKey,\n  name,\n  address,\n  url,\n  telephone,\n  image,\n  priceRange,\n  geo,\n  openingHoursSpecification,\n  review,\n  aggregateRating,\n  department,\n  menu,\n  servesCuisine,\n  sameAs,\n  branchOf,\n  currenciesAccepted,\n  paymentAccepted,\n  areaServed,\n  email,\n  faxNumber,\n  slogan,\n  description,\n  publicAccess,\n  smokingAllowed,\n  isAccessibleForFree,\n}: LocalBusinessJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": type,\n    name,\n    address: Array.isArray(address)\n      ? address.map(processAddress)\n      : processAddress(address),\n    ...(url && { url }),\n    ...(telephone && { telephone }),\n    ...(image && {\n      image: Array.isArray(image)\n        ? image.map(processImage)\n        : processImage(image),\n    }),\n    ...(priceRange && { priceRange }),\n    ...(geo && { geo: processGeo(geo) }),\n    ...(openingHoursSpecification && {\n      openingHoursSpecification: Array.isArray(openingHoursSpecification)\n        ? openingHoursSpecification.map(processOpeningHours)\n        : processOpeningHours(openingHoursSpecification),\n    }),\n    ...(review && {\n      review: Array.isArray(review)\n        ? review.map(processReview)\n        : processReview(review),\n    }),\n    ...(aggregateRating && {\n      aggregateRating: processAggregateRating(aggregateRating),\n    }),\n    ...(department && {\n      department: Array.isArray(department)\n        ? department.map(processDepartment)\n        : processDepartment(department),\n    }),\n    ...(menu && { menu }),\n    ...(servesCuisine && {\n      servesCuisine: Array.isArray(servesCuisine)\n        ? servesCuisine\n        : [servesCuisine],\n    }),\n    ...(sameAs && {\n      sameAs: Array.isArray(sameAs) ? sameAs : [sameAs],\n    }),\n    ...(branchOf && { branchOf }),\n    ...(currenciesAccepted && { currenciesAccepted }),\n    ...(paymentAccepted && { paymentAccepted }),\n    ...(areaServed && {\n      areaServed: Array.isArray(areaServed) ? areaServed : [areaServed],\n    }),\n    ...(email && { email }),\n    ...(faxNumber && { faxNumber }),\n    ...(slogan && { slogan }),\n    ...(description && { description }),\n    ...(publicAccess !== undefined && { publicAccess }),\n    ...(smokingAllowed !== undefined && { smokingAllowed }),\n    ...(isAccessibleForFree !== undefined && { isAccessibleForFree }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={\n        scriptKey ||\n        `localbusiness-jsonld-${Array.isArray(type) ? type.join(\"-\") : type}`\n      }\n    />\n  );\n}\n\nexport type { LocalBusinessJsonLdProps };\n"
  },
  {
    "path": "src/components/MerchantReturnPolicyJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport MerchantReturnPolicyJsonLd from \"./MerchantReturnPolicyJsonLd\";\n\ndescribe(\"MerchantReturnPolicyJsonLd\", () => {\n  it(\"renders basic return policy with minimal props (Option A)\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd\n        applicableCountry=\"US\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n        merchantReturnDays={30}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"MerchantReturnPolicy\",\n      applicableCountry: [\"US\"],\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 30,\n    });\n  });\n\n  it(\"renders return policy with merchantReturnLink only (Option B)\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd merchantReturnLink=\"https://example.com/returns\" />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"MerchantReturnPolicy\",\n      merchantReturnLink: \"https://example.com/returns\",\n    });\n  });\n\n  it(\"handles multiple applicable countries\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd\n        applicableCountry={[\"US\", \"CA\", \"MX\"]}\n        returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n        merchantReturnDays={60}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.applicableCountry).toEqual([\"US\", \"CA\", \"MX\"]);\n  });\n\n  it(\"handles return shipping fees as MonetaryAmount\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd\n        applicableCountry=\"US\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n        merchantReturnDays={30}\n        returnFees=\"https://schema.org/ReturnShippingFees\"\n        returnShippingFeesAmount={{\n          value: 9.99,\n          currency: \"USD\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.returnShippingFeesAmount).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      value: 9.99,\n      currency: \"USD\",\n    });\n  });\n\n  it(\"handles restocking fee as percentage number\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd\n        applicableCountry=\"US\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n        merchantReturnDays={30}\n        restockingFee={15}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.restockingFee).toBe(15);\n  });\n\n  it(\"handles restocking fee as MonetaryAmount\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd\n        applicableCountry=\"US\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n        merchantReturnDays={30}\n        restockingFee={{\n          value: 25,\n          currency: \"USD\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.restockingFee).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      value: 25,\n      currency: \"USD\",\n    });\n  });\n\n  it(\"handles customer remorse specific properties\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd\n        applicableCountry=\"US\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n        merchantReturnDays={30}\n        customerRemorseReturnFees=\"https://schema.org/ReturnShippingFees\"\n        customerRemorseReturnShippingFeesAmount={{\n          value: 5.99,\n          currency: \"EUR\",\n        }}\n        customerRemorseReturnLabelSource=\"https://schema.org/ReturnLabelDownloadAndPrint\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.customerRemorseReturnFees).toBe(\n      \"https://schema.org/ReturnShippingFees\",\n    );\n    expect(jsonData.customerRemorseReturnShippingFeesAmount).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      value: 5.99,\n      currency: \"EUR\",\n    });\n    expect(jsonData.customerRemorseReturnLabelSource).toBe(\n      \"https://schema.org/ReturnLabelDownloadAndPrint\",\n    );\n  });\n\n  it(\"handles item defect specific properties\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd\n        applicableCountry=\"US\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n        merchantReturnDays={30}\n        itemDefectReturnFees=\"https://schema.org/FreeReturn\"\n        itemDefectReturnLabelSource=\"https://schema.org/ReturnLabelInBox\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.itemDefectReturnFees).toBe(\"https://schema.org/FreeReturn\");\n    expect(jsonData.itemDefectReturnLabelSource).toBe(\n      \"https://schema.org/ReturnLabelInBox\",\n    );\n  });\n\n  it(\"handles single seasonal override\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd\n        applicableCountry=\"US\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n        merchantReturnDays={60}\n        returnPolicySeasonalOverride={{\n          startDate: \"2024-11-29\",\n          endDate: \"2024-12-06\",\n          returnPolicyCategory:\n            \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n          merchantReturnDays: 10,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.returnPolicySeasonalOverride).toEqual({\n      \"@type\": \"MerchantReturnPolicySeasonalOverride\",\n      startDate: \"2024-11-29\",\n      endDate: \"2024-12-06\",\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 10,\n    });\n  });\n\n  it(\"handles multiple seasonal overrides\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd\n        applicableCountry=\"US\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnUnlimitedWindow\"\n        returnPolicySeasonalOverride={[\n          {\n            startDate: \"2024-11-29\",\n            endDate: \"2024-12-06\",\n            returnPolicyCategory:\n              \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n            merchantReturnDays: 10,\n          },\n          {\n            startDate: \"2024-12-26\",\n            endDate: \"2025-01-06\",\n            returnPolicyCategory:\n              \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n            merchantReturnDays: 10,\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.returnPolicySeasonalOverride).toHaveLength(2);\n    expect(jsonData.returnPolicySeasonalOverride[0][\"@type\"]).toBe(\n      \"MerchantReturnPolicySeasonalOverride\",\n    );\n    expect(jsonData.returnPolicySeasonalOverride[1][\"@type\"]).toBe(\n      \"MerchantReturnPolicySeasonalOverride\",\n    );\n  });\n\n  it(\"handles all properties comprehensively\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd\n        scriptId=\"return-policy\"\n        scriptKey=\"return-policy-key\"\n        applicableCountry={[\"DE\", \"AT\", \"CH\"]}\n        returnPolicyCountry=\"IE\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnFiniteReturnWindow\"\n        merchantReturnDays={60}\n        itemCondition={[\n          \"https://schema.org/NewCondition\",\n          \"https://schema.org/DamagedCondition\",\n        ]}\n        returnMethod={[\n          \"https://schema.org/ReturnByMail\",\n          \"https://schema.org/ReturnInStore\",\n        ]}\n        returnFees=\"https://schema.org/ReturnShippingFees\"\n        returnShippingFeesAmount={{\n          value: 2.99,\n          currency: \"EUR\",\n        }}\n        refundType={[\n          \"https://schema.org/FullRefund\",\n          \"https://schema.org/ExchangeRefund\",\n        ]}\n        restockingFee={{\n          value: 10,\n          currency: \"EUR\",\n        }}\n        returnLabelSource=\"https://schema.org/ReturnLabelInBox\"\n        customerRemorseReturnFees=\"https://schema.org/ReturnShippingFees\"\n        customerRemorseReturnShippingFeesAmount={{\n          value: 5.99,\n          currency: \"EUR\",\n        }}\n        customerRemorseReturnLabelSource=\"https://schema.org/ReturnLabelDownloadAndPrint\"\n        itemDefectReturnFees=\"https://schema.org/FreeReturn\"\n        itemDefectReturnLabelSource=\"https://schema.org/ReturnLabelInBox\"\n        returnPolicySeasonalOverride={{\n          startDate: \"2025-12-01\",\n          endDate: \"2025-01-05\",\n          returnPolicyCategory:\n            \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n          merchantReturnDays: 30,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n    expect(script!.id).toBe(\"return-policy\");\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"MerchantReturnPolicy\");\n    expect(jsonData.applicableCountry).toEqual([\"DE\", \"AT\", \"CH\"]);\n    expect(jsonData.returnPolicyCountry).toEqual([\"IE\"]);\n    expect(jsonData.merchantReturnDays).toBe(60);\n    expect(jsonData.itemCondition).toEqual([\n      \"https://schema.org/NewCondition\",\n      \"https://schema.org/DamagedCondition\",\n    ]);\n    expect(jsonData.returnMethod).toEqual([\n      \"https://schema.org/ReturnByMail\",\n      \"https://schema.org/ReturnInStore\",\n    ]);\n    expect(jsonData.refundType).toEqual([\n      \"https://schema.org/FullRefund\",\n      \"https://schema.org/ExchangeRefund\",\n    ]);\n  });\n\n  it(\"handles no return policy scenario\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd\n        applicableCountry=\"US\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnNotPermitted\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"MerchantReturnPolicy\",\n      applicableCountry: [\"US\"],\n      returnPolicyCategory: \"https://schema.org/MerchantReturnNotPermitted\",\n    });\n  });\n\n  it(\"handles unlimited return window\", () => {\n    const { container } = render(\n      <MerchantReturnPolicyJsonLd\n        applicableCountry=\"US\"\n        returnPolicyCategory=\"https://schema.org/MerchantReturnUnlimitedWindow\"\n        returnMethod=\"https://schema.org/ReturnByMail\"\n        refundType=\"https://schema.org/FullRefund\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.returnPolicyCategory).toBe(\n      \"https://schema.org/MerchantReturnUnlimitedWindow\",\n    );\n    expect(jsonData.merchantReturnDays).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "src/components/MerchantReturnPolicyJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { MerchantReturnPolicyJsonLdProps } from \"~/types/merchantreturnpolicy.types\";\nimport {\n  processMerchantReturnPolicy,\n  processSimpleMonetaryAmount,\n  processReturnPolicySeasonalOverride,\n} from \"~/utils/processors\";\n\nexport default function MerchantReturnPolicyJsonLd({\n  scriptId,\n  scriptKey,\n  applicableCountry,\n  returnPolicyCountry,\n  returnPolicyCategory,\n  merchantReturnDays,\n  returnMethod,\n  returnFees,\n  returnShippingFeesAmount,\n  refundType,\n  restockingFee,\n  returnLabelSource,\n  itemCondition,\n  customerRemorseReturnFees,\n  customerRemorseReturnShippingFeesAmount,\n  customerRemorseReturnLabelSource,\n  itemDefectReturnFees,\n  itemDefectReturnShippingFeesAmount,\n  itemDefectReturnLabelSource,\n  returnPolicySeasonalOverride,\n  merchantReturnLink,\n}: MerchantReturnPolicyJsonLdProps) {\n  // Build the return policy object\n  const returnPolicy = {\n    \"@type\": \"MerchantReturnPolicy\" as const,\n    // Option B: Simple link to policy\n    ...(merchantReturnLink && { merchantReturnLink }),\n    // Option A: Detailed properties\n    ...(applicableCountry && {\n      applicableCountry: Array.isArray(applicableCountry)\n        ? applicableCountry\n        : [applicableCountry],\n    }),\n    ...(returnPolicyCountry && {\n      returnPolicyCountry: Array.isArray(returnPolicyCountry)\n        ? returnPolicyCountry\n        : [returnPolicyCountry],\n    }),\n    ...(returnPolicyCategory && { returnPolicyCategory }),\n    ...(merchantReturnDays !== undefined && { merchantReturnDays }),\n    ...(returnMethod && {\n      returnMethod: Array.isArray(returnMethod) ? returnMethod : [returnMethod],\n    }),\n    ...(returnFees && { returnFees }),\n    ...(returnShippingFeesAmount && {\n      returnShippingFeesAmount: processSimpleMonetaryAmount(\n        returnShippingFeesAmount,\n      ),\n    }),\n    ...(refundType && {\n      refundType: Array.isArray(refundType) ? refundType : [refundType],\n    }),\n    ...(restockingFee !== undefined && {\n      restockingFee:\n        typeof restockingFee === \"number\"\n          ? restockingFee\n          : processSimpleMonetaryAmount(restockingFee),\n    }),\n    ...(returnLabelSource && { returnLabelSource }),\n    ...(itemCondition && {\n      itemCondition: Array.isArray(itemCondition)\n        ? itemCondition\n        : [itemCondition],\n    }),\n    // Customer remorse specific properties\n    ...(customerRemorseReturnFees && { customerRemorseReturnFees }),\n    ...(customerRemorseReturnShippingFeesAmount && {\n      customerRemorseReturnShippingFeesAmount: processSimpleMonetaryAmount(\n        customerRemorseReturnShippingFeesAmount,\n      ),\n    }),\n    ...(customerRemorseReturnLabelSource && {\n      customerRemorseReturnLabelSource,\n    }),\n    // Item defect specific properties\n    ...(itemDefectReturnFees && { itemDefectReturnFees }),\n    ...(itemDefectReturnShippingFeesAmount && {\n      itemDefectReturnShippingFeesAmount: processSimpleMonetaryAmount(\n        itemDefectReturnShippingFeesAmount,\n      ),\n    }),\n    ...(itemDefectReturnLabelSource && { itemDefectReturnLabelSource }),\n    // Seasonal override\n    ...(returnPolicySeasonalOverride && {\n      returnPolicySeasonalOverride: Array.isArray(returnPolicySeasonalOverride)\n        ? returnPolicySeasonalOverride.map(processReturnPolicySeasonalOverride)\n        : processReturnPolicySeasonalOverride(returnPolicySeasonalOverride),\n    }),\n  };\n\n  // Process the entire policy to ensure all nested types are correct\n  const processedPolicy = processMerchantReturnPolicy(returnPolicy);\n\n  const data = {\n    \"@context\": \"https://schema.org\",\n    ...processedPolicy,\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"merchant-return-policy-jsonld\"}\n    />\n  );\n}\n\nexport type { MerchantReturnPolicyJsonLdProps };\n"
  },
  {
    "path": "src/components/MovieCarouselJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport MovieCarouselJsonLd from \"./MovieCarouselJsonLd\";\n\ndescribe(\"MovieCarouselJsonLd\", () => {\n  describe(\"Summary Page Pattern\", () => {\n    it(\"renders basic summary page with string URLs\", () => {\n      const { container } = render(\n        <MovieCarouselJsonLd\n          urls={[\n            \"https://example.com/movie1\",\n            \"https://example.com/movie2\",\n            \"https://example.com/movie3\",\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script).toBeTruthy();\n\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData).toEqual({\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"ItemList\",\n        itemListElement: [\n          {\n            \"@type\": \"ListItem\",\n            position: 1,\n            url: \"https://example.com/movie1\",\n          },\n          {\n            \"@type\": \"ListItem\",\n            position: 2,\n            url: \"https://example.com/movie2\",\n          },\n          {\n            \"@type\": \"ListItem\",\n            position: 3,\n            url: \"https://example.com/movie3\",\n          },\n        ],\n      });\n    });\n\n    it(\"renders summary page with object URLs and custom positions\", () => {\n      const { container } = render(\n        <MovieCarouselJsonLd\n          urls={[\n            { url: \"https://example.com/movie1\", position: 3 },\n            { url: \"https://example.com/movie2\", position: 1 },\n            { url: \"https://example.com/movie3\", position: 2 },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      expect(jsonData.itemListElement[0].position).toBe(3);\n      expect(jsonData.itemListElement[1].position).toBe(1);\n      expect(jsonData.itemListElement[2].position).toBe(2);\n    });\n\n    it(\"renders summary page with mixed URL formats\", () => {\n      const { container } = render(\n        <MovieCarouselJsonLd\n          urls={[\n            \"https://example.com/movie1\",\n            { url: \"https://example.com/movie2\", position: 5 },\n            \"https://example.com/movie3\",\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      expect(jsonData.itemListElement[0].position).toBe(1);\n      expect(jsonData.itemListElement[1].position).toBe(5);\n      expect(jsonData.itemListElement[2].position).toBe(3);\n    });\n  });\n\n  describe(\"All-in-One Page Pattern\", () => {\n    it(\"renders basic all-in-one page with minimal movie data\", () => {\n      const { container } = render(\n        <MovieCarouselJsonLd\n          movies={[\n            {\n              name: \"A Star Is Born\",\n              image: \"https://example.com/star-is-born.jpg\",\n            },\n            {\n              name: \"Bohemian Rhapsody\",\n              image: \"https://example.com/bohemian.jpg\",\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script).toBeTruthy();\n\n      const jsonData = JSON.parse(script!.textContent!);\n      expect(jsonData).toEqual({\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"ItemList\",\n        itemListElement: [\n          {\n            \"@type\": \"ListItem\",\n            position: 1,\n            item: {\n              \"@type\": \"Movie\",\n              name: \"A Star Is Born\",\n              image: \"https://example.com/star-is-born.jpg\",\n            },\n          },\n          {\n            \"@type\": \"ListItem\",\n            position: 2,\n            item: {\n              \"@type\": \"Movie\",\n              name: \"Bohemian Rhapsody\",\n              image: \"https://example.com/bohemian.jpg\",\n            },\n          },\n        ],\n      });\n    });\n\n    it(\"handles string director and converts to Person\", () => {\n      const { container } = render(\n        <MovieCarouselJsonLd\n          movies={[\n            {\n              name: \"A Star Is Born\",\n              image: \"https://example.com/star-is-born.jpg\",\n              director: \"Bradley Cooper\",\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      expect(jsonData.itemListElement[0].item.director).toEqual({\n        \"@type\": \"Person\",\n        name: \"Bradley Cooper\",\n      });\n    });\n\n    it(\"handles director as Person object without @type\", () => {\n      const { container } = render(\n        <MovieCarouselJsonLd\n          movies={[\n            {\n              name: \"A Star Is Born\",\n              image: \"https://example.com/star-is-born.jpg\",\n              director: {\n                name: \"Bradley Cooper\",\n                url: \"https://example.com/bradley-cooper\",\n              },\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      expect(jsonData.itemListElement[0].item.director).toEqual({\n        \"@type\": \"Person\",\n        name: \"Bradley Cooper\",\n        url: \"https://example.com/bradley-cooper\",\n      });\n    });\n\n    it(\"handles multiple images as array\", () => {\n      const { container } = render(\n        <MovieCarouselJsonLd\n          movies={[\n            {\n              name: \"Black Panther\",\n              image: [\n                \"https://example.com/black-panther-1.jpg\",\n                {\n                  url: \"https://example.com/black-panther-2.jpg\",\n                  width: 1200,\n                  height: 1800,\n                },\n              ],\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      expect(jsonData.itemListElement[0].item.image).toEqual([\n        \"https://example.com/black-panther-1.jpg\",\n        {\n          \"@type\": \"ImageObject\",\n          url: \"https://example.com/black-panther-2.jpg\",\n          width: 1200,\n          height: 1800,\n        },\n      ]);\n    });\n\n    it(\"handles review with rating\", () => {\n      const { container } = render(\n        <MovieCarouselJsonLd\n          movies={[\n            {\n              name: \"A Star Is Born\",\n              image: \"https://example.com/star-is-born.jpg\",\n              review: {\n                reviewRating: {\n                  ratingValue: 5,\n                },\n                author: \"John D.\",\n              },\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      expect(jsonData.itemListElement[0].item.review).toEqual({\n        \"@type\": \"Review\",\n        reviewRating: {\n          \"@type\": \"Rating\",\n          ratingValue: 5,\n        },\n        author: {\n          \"@type\": \"Person\",\n          name: \"John D.\",\n        },\n      });\n    });\n\n    it(\"handles aggregateRating\", () => {\n      const { container } = render(\n        <MovieCarouselJsonLd\n          movies={[\n            {\n              name: \"Black Panther\",\n              image: \"https://example.com/black-panther.jpg\",\n              aggregateRating: {\n                ratingValue: 96,\n                bestRating: 100,\n                ratingCount: 88211,\n              },\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n\n      expect(jsonData.itemListElement[0].item.aggregateRating).toEqual({\n        \"@type\": \"AggregateRating\",\n        ratingValue: 96,\n        bestRating: 100,\n        ratingCount: 88211,\n      });\n    });\n\n    it(\"handles all optional properties\", () => {\n      const { container } = render(\n        <MovieCarouselJsonLd\n          movies={[\n            {\n              name: \"A Star Is Born\",\n              image: \"https://example.com/star-is-born.jpg\",\n              url: \"https://example.com/movies/a-star-is-born\",\n              dateCreated: \"2024-10-05\",\n              director: \"Bradley Cooper\",\n              review: {\n                reviewRating: {\n                  ratingValue: 5,\n                },\n                author: {\n                  name: \"John D.\",\n                },\n              },\n              aggregateRating: {\n                ratingValue: 90,\n                bestRating: 100,\n                ratingCount: 19141,\n              },\n            },\n          ]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      const jsonData = JSON.parse(script!.textContent!);\n      const movie = jsonData.itemListElement[0].item;\n\n      expect(movie[\"@type\"]).toBe(\"Movie\");\n      expect(movie.name).toBe(\"A Star Is Born\");\n      expect(movie.image).toBe(\"https://example.com/star-is-born.jpg\");\n      expect(movie.url).toBe(\"https://example.com/movies/a-star-is-born\");\n      expect(movie.dateCreated).toBe(\"2024-10-05\");\n      expect(movie.director).toEqual({\n        \"@type\": \"Person\",\n        name: \"Bradley Cooper\",\n      });\n      expect(movie.review).toEqual({\n        \"@type\": \"Review\",\n        reviewRating: {\n          \"@type\": \"Rating\",\n          ratingValue: 5,\n        },\n        author: {\n          \"@type\": \"Person\",\n          name: \"John D.\",\n        },\n      });\n      expect(movie.aggregateRating).toEqual({\n        \"@type\": \"AggregateRating\",\n        ratingValue: 90,\n        bestRating: 100,\n        ratingCount: 19141,\n      });\n    });\n\n    it(\"uses custom scriptId and scriptKey\", () => {\n      const { container } = render(\n        <MovieCarouselJsonLd\n          scriptId=\"custom-movie-id\"\n          scriptKey=\"custom-movie-key\"\n          urls={[\"https://example.com/movie1\"]}\n        />,\n      );\n\n      const script = container.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      expect(script?.getAttribute(\"id\")).toBe(\"custom-movie-id\");\n      // Note: scriptKey is internal to JsonLdScript component\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/MovieCarouselJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type {\n  MovieCarouselJsonLdProps,\n  MovieListItem,\n  SummaryPageItem,\n  MovieCarouselListItem,\n  Movie,\n} from \"~/types/movie-carousel.types\";\nimport {\n  processImage,\n  processDirector,\n  processReview,\n  processAggregateRating,\n} from \"~/utils/processors\";\n\nfunction processSummaryItem(\n  item: SummaryPageItem,\n  index: number,\n): MovieCarouselListItem {\n  if (typeof item === \"string\") {\n    return {\n      \"@type\": \"ListItem\",\n      position: index + 1,\n      url: item,\n    };\n  }\n  return {\n    \"@type\": \"ListItem\",\n    position: item.position ?? index + 1,\n    url: item.url,\n  };\n}\n\nfunction processMovieItem(\n  movie: MovieListItem,\n  index: number,\n): MovieCarouselListItem {\n  const processedMovie: Movie = {\n    \"@type\": \"Movie\",\n    name: movie.name,\n    image: Array.isArray(movie.image)\n      ? movie.image.map(processImage)\n      : processImage(movie.image),\n    ...(movie.url && { url: movie.url }),\n    ...(movie.dateCreated && { dateCreated: movie.dateCreated }),\n    ...(movie.director && { director: processDirector(movie.director) }),\n    ...(movie.review && { review: processReview(movie.review) }),\n    ...(movie.aggregateRating && {\n      aggregateRating: processAggregateRating(movie.aggregateRating),\n    }),\n  };\n\n  return {\n    \"@type\": \"ListItem\",\n    position: index + 1,\n    item: processedMovie,\n  };\n}\n\nexport default function MovieCarouselJsonLd(props: MovieCarouselJsonLdProps) {\n  const { scriptId, scriptKey } = props;\n\n  // Determine if this is summary page or all-in-one page pattern\n  const isSummaryPage = \"urls\" in props;\n\n  let itemListElement: MovieCarouselListItem[];\n\n  if (isSummaryPage) {\n    // Summary page pattern - just URLs\n    itemListElement = props.urls.map((url, index) =>\n      processSummaryItem(url, index),\n    );\n  } else {\n    // All-in-one page pattern - full movie data\n    itemListElement = props.movies.map((movie, index) =>\n      processMovieItem(movie, index),\n    );\n  }\n\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"ItemList\",\n    itemListElement,\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"movie-carousel-jsonld\"}\n    />\n  );\n}\n\nexport type { MovieCarouselJsonLdProps };\n"
  },
  {
    "path": "src/components/OrganizationJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport OrganizationJsonLd from \"./OrganizationJsonLd\";\nimport type { TierBenefit } from \"~/types/common.types\";\n\ndescribe(\"OrganizationJsonLd\", () => {\n  it(\"renders basic Organization with minimal props\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corporation\"\n        url=\"https://www.example.com\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Organization\",\n      name: \"Example Corporation\",\n      url: \"https://www.example.com\",\n    });\n  });\n\n  it(\"preserves URL query parameters\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        url=\"https://www.example.com?utm_source=google&campaign=2024\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.url).toBe(\n      \"https://www.example.com?utm_source=google&campaign=2024\",\n    );\n  });\n\n  it(\"renders OnlineStore type when specified\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        type=\"OnlineStore\"\n        name=\"Example Online Store\"\n        url=\"https://www.example.com\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toBe(\"OnlineStore\");\n  });\n\n  it(\"handles string address\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        address=\"123 Main St, New York, NY 10001\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.address).toEqual({\n      \"@type\": \"PostalAddress\",\n      streetAddress: \"123 Main St, New York, NY 10001\",\n    });\n  });\n\n  it(\"handles PostalAddress object\", () => {\n    const address = {\n      \"@type\": \"PostalAddress\" as const,\n      streetAddress: \"999 W Example St Suite 99\",\n      addressLocality: \"New York\",\n      addressRegion: \"NY\",\n      postalCode: \"10019\",\n      addressCountry: \"US\",\n    };\n\n    const { container } = render(\n      <OrganizationJsonLd name=\"Example Corp\" address={address} />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.address).toEqual(address);\n  });\n\n  it(\"handles multiple addresses\", () => {\n    const addresses = [\n      \"123 Main St, New York, NY 10001\",\n      {\n        \"@type\": \"PostalAddress\" as const,\n        streetAddress: \"999 Rue du exemple\",\n        addressLocality: \"Paris\",\n        postalCode: \"75001\",\n        addressCountry: \"FR\",\n      },\n    ];\n\n    const { container } = render(\n      <OrganizationJsonLd name=\"Example Corp\" address={addresses} />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.address).toHaveLength(2);\n    expect(jsonData.address[0]).toEqual({\n      \"@type\": \"PostalAddress\",\n      streetAddress: \"123 Main St, New York, NY 10001\",\n    });\n    expect(jsonData.address[1][\"@type\"]).toBe(\"PostalAddress\");\n  });\n\n  it(\"handles single sameAs URL\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        sameAs=\"https://twitter.com/example\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.sameAs).toEqual([\"https://twitter.com/example\"]);\n  });\n\n  it(\"handles multiple sameAs URLs\", () => {\n    const sameAs = [\n      \"https://twitter.com/example\",\n      \"https://facebook.com/example\",\n    ];\n\n    const { container } = render(\n      <OrganizationJsonLd name=\"Example Corp\" sameAs={sameAs} />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.sameAs).toEqual(sameAs);\n  });\n\n  it(\"handles string logo\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        logo=\"https://www.example.com/logo.png\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.logo).toBe(\"https://www.example.com/logo.png\");\n  });\n\n  it(\"handles ImageObject logo\", () => {\n    const logo = {\n      \"@type\": \"ImageObject\" as const,\n      url: \"https://www.example.com/logo.png\",\n      width: 600,\n      height: 400,\n    };\n\n    const { container } = render(\n      <OrganizationJsonLd name=\"Example Corp\" logo={logo} />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.logo).toEqual(logo);\n  });\n\n  it(\"handles contactPoint\", () => {\n    const contactPoint = {\n      \"@type\": \"ContactPoint\" as const,\n      contactType: \"Customer Service\",\n      telephone: \"+1-999-999-9999\",\n      email: \"support@example.com\",\n    };\n\n    const { container } = render(\n      <OrganizationJsonLd name=\"Example Corp\" contactPoint={contactPoint} />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.contactPoint).toEqual(contactPoint);\n  });\n\n  it(\"handles number of employees as number\", () => {\n    const { container } = render(\n      <OrganizationJsonLd name=\"Example Corp\" numberOfEmployees={100} />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.numberOfEmployees).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 100,\n    });\n  });\n\n  it(\"handles number of employees as range\", () => {\n    const numberOfEmployees = {\n      \"@type\": \"QuantitativeValue\" as const,\n      minValue: 100,\n      maxValue: 999,\n    };\n\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        numberOfEmployees={numberOfEmployees}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.numberOfEmployees).toEqual(numberOfEmployees);\n  });\n\n  it(\"handles number of employees as range without @type\", () => {\n    const numberOfEmployees = {\n      minValue: 100,\n      maxValue: 999,\n    };\n\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        numberOfEmployees={numberOfEmployees}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.numberOfEmployees).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      minValue: 100,\n      maxValue: 999,\n    });\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corporation\"\n        url=\"https://www.example.com\"\n        logo=\"https://www.example.com/logo.png\"\n        description=\"The example corporation is well-known for producing high-quality widgets\"\n        sameAs={[\n          \"https://example.net/profile/example1234\",\n          \"https://example.org/example1234\",\n        ]}\n        address={{\n          \"@type\": \"PostalAddress\",\n          streetAddress: \"Rue Improbable 99\",\n          addressLocality: \"Paris\",\n          addressCountry: \"FR\",\n          addressRegion: \"Ile-de-France\",\n          postalCode: \"75001\",\n        }}\n        contactPoint={{\n          \"@type\": \"ContactPoint\",\n          contactType: \"Customer Service\",\n          telephone: \"+47-99-999-9999\",\n          email: \"contact@example.com\",\n        }}\n        telephone=\"+47-99-999-9999\"\n        email=\"contact@example.com\"\n        alternateName=\"Example Corp\"\n        foundingDate=\"2010-01-01\"\n        legalName=\"Example Corporation Inc.\"\n        taxID=\"123456789\"\n        vatID=\"FR12345678901\"\n        duns=\"123456789\"\n        leiCode=\"529900T8BM49AURSDO55\"\n        naics=\"54151\"\n        globalLocationNumber=\"1234567890123\"\n        iso6523Code=\"0199:724500PMK2A2M1SQQ228\"\n        numberOfEmployees={2056}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.name).toBe(\"Example Corporation\");\n    expect(jsonData.url).toBe(\"https://www.example.com\");\n    expect(jsonData.logo).toBe(\"https://www.example.com/logo.png\");\n    expect(jsonData.description).toBe(\n      \"The example corporation is well-known for producing high-quality widgets\",\n    );\n    expect(jsonData.sameAs).toHaveLength(2);\n    expect(jsonData.address[\"@type\"]).toBe(\"PostalAddress\");\n    expect(jsonData.contactPoint[\"@type\"]).toBe(\"ContactPoint\");\n    expect(jsonData.telephone).toBe(\"+47-99-999-9999\");\n    expect(jsonData.email).toBe(\"contact@example.com\");\n    expect(jsonData.alternateName).toBe(\"Example Corp\");\n    expect(jsonData.foundingDate).toBe(\"2010-01-01\");\n    expect(jsonData.legalName).toBe(\"Example Corporation Inc.\");\n    expect(jsonData.taxID).toBe(\"123456789\");\n    expect(jsonData.vatID).toBe(\"FR12345678901\");\n    expect(jsonData.duns).toBe(\"123456789\");\n    expect(jsonData.leiCode).toBe(\"529900T8BM49AURSDO55\");\n    expect(jsonData.naics).toBe(\"54151\");\n    expect(jsonData.globalLocationNumber).toBe(\"1234567890123\");\n    expect(jsonData.iso6523Code).toBe(\"0199:724500PMK2A2M1SQQ228\");\n    expect(jsonData.numberOfEmployees.value).toBe(2056);\n  });\n\n  it(\"handles OnlineStore with merchant return policy\", () => {\n    const merchantReturnPolicy = {\n      \"@type\": \"MerchantReturnPolicy\" as const,\n      applicableCountry: [\"FR\", \"CH\"],\n      returnPolicyCountry: \"FR\",\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 60,\n      returnMethod: \"https://schema.org/ReturnByMail\",\n      returnFees: \"https://schema.org/FreeReturn\",\n      refundType: \"https://schema.org/FullRefund\",\n    };\n\n    const { container } = render(\n      <OrganizationJsonLd\n        type=\"OnlineStore\"\n        name=\"Example Online Store\"\n        hasMerchantReturnPolicy={merchantReturnPolicy}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toBe(\"OnlineStore\");\n    expect(jsonData.hasMerchantReturnPolicy).toEqual(merchantReturnPolicy);\n  });\n\n  it(\"handles OnlineStore with member program\", () => {\n    const memberProgram = {\n      \"@type\": \"MemberProgram\" as const,\n      name: \"Membership Plus\",\n      description:\n        \"For frequent shoppers this is our top-rated loyalty program\",\n      url: \"https://www.example.com/membership-plus\",\n      hasTiers: [\n        {\n          \"@type\": \"MemberProgramTier\" as const,\n          \"@id\": \"#plus-tier-silver\",\n          name: \"silver\",\n          url: \"https://www.example.com/membership-plus-silver\",\n          hasTierBenefit: [\n            \"https://schema.org/TierBenefitLoyaltyPoints\" as TierBenefit,\n          ],\n          membershipPointsEarned: 5,\n        },\n      ],\n    };\n\n    const { container } = render(\n      <OrganizationJsonLd\n        type=\"OnlineStore\"\n        name=\"Example Online Store\"\n        hasMemberProgram={memberProgram}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@type\"]).toBe(\"OnlineStore\");\n    expect(jsonData.hasMemberProgram).toEqual(memberProgram);\n  });\n\n  it(\"handles MemberProgram without @type\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        type=\"OnlineStore\"\n        name=\"Example Online Store\"\n        hasMemberProgram={{\n          name: \"Rewards Club\",\n          description: \"Earn points on every purchase\",\n          hasTiers: {\n            name: \"bronze\",\n            hasTierBenefit: \"TierBenefitLoyaltyPoints\",\n            membershipPointsEarned: 2,\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.hasMemberProgram[\"@type\"]).toBe(\"MemberProgram\");\n    expect(jsonData.hasMemberProgram.name).toBe(\"Rewards Club\");\n    expect(jsonData.hasMemberProgram.hasTiers[\"@type\"]).toBe(\n      \"MemberProgramTier\",\n    );\n    expect(jsonData.hasMemberProgram.hasTiers.hasTierBenefit).toBe(\n      \"https://schema.org/TierBenefitLoyaltyPoints\",\n    );\n    expect(jsonData.hasMemberProgram.hasTiers.membershipPointsEarned).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 2,\n    });\n  });\n\n  it(\"handles MemberProgram with multiple tiers and requirements\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        type=\"OnlineStore\"\n        name=\"Example Online Store\"\n        hasMemberProgram={{\n          name: \"Premium Rewards\",\n          description: \"Multi-tier loyalty program\",\n          url: \"https://example.com/rewards\",\n          hasTiers: [\n            {\n              name: \"silver\",\n              hasTierBenefit: [\"TierBenefitLoyaltyPoints\"],\n              membershipPointsEarned: 5,\n            },\n            {\n              name: \"gold\",\n              hasTierBenefit: [\n                \"TierBenefitLoyaltyPoints\",\n                \"TierBenefitLoyaltyPrice\",\n              ],\n              hasTierRequirement: {\n                name: \"Example Gold Card\",\n              },\n              membershipPointsEarned: 10,\n            },\n            {\n              name: \"platinum\",\n              hasTierBenefit: [\n                \"TierBenefitLoyaltyPoints\",\n                \"TierBenefitLoyaltyPrice\",\n              ],\n              hasTierRequirement: {\n                value: 1000,\n                currency: \"USD\",\n              },\n              membershipPointsEarned: {\n                value: 15,\n                unitText: \"points per dollar\",\n              },\n            },\n          ],\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    const program = jsonData.hasMemberProgram;\n    expect(program[\"@type\"]).toBe(\"MemberProgram\");\n    expect(program.hasTiers).toHaveLength(3);\n\n    // Check silver tier (no requirement)\n    expect(program.hasTiers[0][\"@type\"]).toBe(\"MemberProgramTier\");\n    expect(program.hasTiers[0].name).toBe(\"silver\");\n    expect(program.hasTiers[0].hasTierRequirement).toBeUndefined();\n\n    // Check gold tier (CreditCard requirement)\n    expect(program.hasTiers[1].hasTierRequirement).toEqual({\n      \"@type\": \"CreditCard\",\n      name: \"Example Gold Card\",\n    });\n\n    // Check platinum tier (MonetaryAmount requirement)\n    expect(program.hasTiers[2].hasTierRequirement).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      value: 1000,\n      currency: \"USD\",\n    });\n    expect(program.hasTiers[2].membershipPointsEarned).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 15,\n      unitText: \"points per dollar\",\n    });\n  });\n\n  it(\"handles tier requirement as UnitPriceSpecification\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        type=\"OnlineStore\"\n        name=\"Example Online Store\"\n        hasMemberProgram={{\n          name: \"Subscription Rewards\",\n          description: \"Rewards for subscribers\",\n          hasTiers: {\n            name: \"premium\",\n            hasTierBenefit: \"TierBenefitLoyaltyPrice\",\n            hasTierRequirement: {\n              price: 9.99,\n              priceCurrency: \"EUR\",\n              billingDuration: 12,\n              billingIncrement: 1,\n              unitCode: \"MON\",\n            },\n            membershipPointsEarned: 100,\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    const tier = jsonData.hasMemberProgram.hasTiers;\n    expect(tier.hasTierRequirement).toEqual({\n      \"@type\": \"UnitPriceSpecification\",\n      price: 9.99,\n      priceCurrency: \"EUR\",\n      billingDuration: 12,\n      billingIncrement: 1,\n      unitCode: \"MON\",\n    });\n  });\n\n  it(\"handles tier requirement as text description\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        type=\"OnlineStore\"\n        name=\"Example Online Store\"\n        hasMemberProgram={{\n          name: \"Community Program\",\n          description: \"Community-based rewards\",\n          hasTiers: {\n            name: \"volunteer\",\n            hasTierBenefit: \"TierBenefitLoyaltyPrice\",\n            hasTierRequirement:\n              \"Purchase a share in our coop and volunteer a minimum of 1 day a month\",\n            membershipPointsEarned: 50,\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    const tier = jsonData.hasMemberProgram.hasTiers;\n    expect(tier.hasTierRequirement).toBe(\n      \"Purchase a share in our coop and volunteer a minimum of 1 day a month\",\n    );\n  });\n\n  it(\"handles array of member programs\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        type=\"OnlineStore\"\n        name=\"Example Online Store\"\n        hasMemberProgram={[\n          {\n            name: \"Basic Rewards\",\n            description: \"Simple points program\",\n            hasTiers: {\n              name: \"member\",\n              hasTierBenefit: \"TierBenefitLoyaltyPoints\",\n              membershipPointsEarned: 1,\n            },\n          },\n          {\n            name: \"Premium Club\",\n            description: \"Exclusive benefits\",\n            hasTiers: {\n              name: \"vip\",\n              hasTierBenefit: [\n                \"TierBenefitLoyaltyPoints\",\n                \"TierBenefitLoyaltyPrice\",\n              ],\n              membershipPointsEarned: 5,\n            },\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.hasMemberProgram).toHaveLength(2);\n    expect(jsonData.hasMemberProgram[0][\"@type\"]).toBe(\"MemberProgram\");\n    expect(jsonData.hasMemberProgram[0].name).toBe(\"Basic Rewards\");\n    expect(jsonData.hasMemberProgram[1][\"@type\"]).toBe(\"MemberProgram\");\n    expect(jsonData.hasMemberProgram[1].name).toBe(\"Premium Club\");\n  });\n\n  it(\"handles single review with string author\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        review={{\n          author: \"Jane Doe\",\n          reviewBody: \"Excellent company to work with!\",\n          reviewRating: {\n            ratingValue: 5,\n            bestRating: 5,\n          },\n          datePublished: \"2025-01-15\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.review).toEqual({\n      \"@type\": \"Review\",\n      author: { \"@type\": \"Person\", name: \"Jane Doe\" },\n      reviewBody: \"Excellent company to work with!\",\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 5,\n        bestRating: 5,\n      },\n      datePublished: \"2025-01-15\",\n    });\n  });\n\n  it(\"handles multiple reviews\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        review={[\n          {\n            author: \"Jane Doe\",\n            reviewBody: \"Excellent company!\",\n            reviewRating: {\n              ratingValue: 5,\n            },\n          },\n          {\n            author: \"John Smith\",\n            reviewBody: \"Great service.\",\n            reviewRating: {\n              ratingValue: 4,\n            },\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.review).toHaveLength(2);\n    expect(jsonData.review[0][\"@type\"]).toBe(\"Review\");\n    expect(jsonData.review[0].author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Jane Doe\",\n    });\n    expect(jsonData.review[1][\"@type\"]).toBe(\"Review\");\n    expect(jsonData.review[1].author).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Smith\",\n    });\n  });\n\n  it(\"handles review with @type already provided\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        review={{\n          \"@type\": \"Review\" as const,\n          author: \"Jane Doe\",\n          reviewBody: \"Great organization!\",\n          reviewRating: {\n            \"@type\": \"Rating\" as const,\n            ratingValue: 4,\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.review[\"@type\"]).toBe(\"Review\");\n    expect(jsonData.review.reviewRating[\"@type\"]).toBe(\"Rating\");\n  });\n\n  it(\"handles aggregateRating without @type\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        aggregateRating={{\n          ratingValue: 4.5,\n          ratingCount: 120,\n          reviewCount: 89,\n          bestRating: 5,\n          worstRating: 1,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.5,\n      ratingCount: 120,\n      reviewCount: 89,\n      bestRating: 5,\n      worstRating: 1,\n    });\n  });\n\n  it(\"handles aggregateRating with @type\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        aggregateRating={{\n          \"@type\": \"AggregateRating\" as const,\n          ratingValue: 4.2,\n          ratingCount: 50,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.2,\n      ratingCount: 50,\n    });\n  });\n\n  it(\"handles both review and aggregateRating together\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        url=\"https://www.example.com\"\n        review={{\n          author: \"Jane Doe\",\n          reviewBody: \"Outstanding organization!\",\n          reviewRating: {\n            ratingValue: 5,\n          },\n        }}\n        aggregateRating={{\n          ratingValue: 4.7,\n          ratingCount: 200,\n          reviewCount: 150,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.review[\"@type\"]).toBe(\"Review\");\n    expect(jsonData.review.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Jane Doe\",\n    });\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.7,\n      ratingCount: 200,\n      reviewCount: 150,\n    });\n  });\n\n  it(\"does not include merchant properties for Organization type\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        type=\"Organization\"\n        name=\"Example Corp\"\n        hasMerchantReturnPolicy={{\n          \"@type\": \"MerchantReturnPolicy\",\n          returnPolicyCountry: \"US\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.hasMerchantReturnPolicy).toBeUndefined();\n  });\n\n  it(\"handles custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Example Corp\"\n        scriptId=\"custom-id\"\n        scriptKey=\"custom-key\"\n      />,\n    );\n\n    const script = container.querySelector('script[id=\"custom-id\"]');\n    expect(script).toBeTruthy();\n  });\n\n  it(\"handles array of contact points\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        name=\"Multi-Contact Corp\"\n        contactPoint={[\n          {\n            telephone: \"+1-800-SALES\",\n            contactType: \"sales\",\n          },\n          {\n            telephone: \"+1-800-SUPPORT\",\n            contactType: \"customer support\",\n          },\n          {\n            telephone: \"+44-20-1234-5678\",\n            contactType: \"sales\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.contactPoint).toHaveLength(3);\n    expect(jsonData.contactPoint[0]).toEqual({\n      \"@type\": \"ContactPoint\",\n      telephone: \"+1-800-SALES\",\n      contactType: \"sales\",\n    });\n    expect(jsonData.contactPoint[1]).toEqual({\n      \"@type\": \"ContactPoint\",\n      telephone: \"+1-800-SUPPORT\",\n      contactType: \"customer support\",\n    });\n    expect(jsonData.contactPoint[2]).toEqual({\n      \"@type\": \"ContactPoint\",\n      telephone: \"+44-20-1234-5678\",\n      contactType: \"sales\",\n    });\n  });\n\n  it(\"handles OnlineStore with array of merchant return policies\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        type=\"OnlineStore\"\n        name=\"Multi-Policy Store\"\n        hasMerchantReturnPolicy={[\n          {\n            applicableCountry: \"US\",\n            returnPolicyCategory:\n              \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n            merchantReturnDays: 30,\n            returnMethod: \"https://schema.org/ReturnByMail\",\n          },\n          {\n            applicableCountry: \"CA\",\n            returnPolicyCategory:\n              \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n            merchantReturnDays: 60,\n            returnMethod: \"https://schema.org/ReturnInStore\",\n            returnFees: \"https://schema.org/FreeReturn\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"OnlineStore\");\n    expect(jsonData.hasMerchantReturnPolicy).toHaveLength(2);\n    expect(jsonData.hasMerchantReturnPolicy[0]).toEqual({\n      \"@type\": \"MerchantReturnPolicy\",\n      applicableCountry: [\"US\"],\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 30,\n      returnMethod: [\"https://schema.org/ReturnByMail\"],\n    });\n    expect(jsonData.hasMerchantReturnPolicy[1]).toEqual({\n      \"@type\": \"MerchantReturnPolicy\",\n      applicableCountry: [\"CA\"],\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 60,\n      returnMethod: [\"https://schema.org/ReturnInStore\"],\n      returnFees: \"https://schema.org/FreeReturn\",\n    });\n  });\n\n  it(\"handles OnlineStore with enhanced merchant return policy features\", () => {\n    const { container } = render(\n      <OrganizationJsonLd\n        type=\"OnlineStore\"\n        name=\"Advanced Online Store\"\n        hasMerchantReturnPolicy={{\n          applicableCountry: [\"DE\", \"AT\", \"CH\"],\n          returnPolicyCountry: \"IE\",\n          returnPolicyCategory:\n            \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n          merchantReturnDays: 60,\n          returnFees: \"https://schema.org/ReturnShippingFees\",\n          returnShippingFeesAmount: {\n            value: 2.99,\n            currency: \"EUR\",\n          },\n          customerRemorseReturnFees: \"https://schema.org/ReturnShippingFees\",\n          customerRemorseReturnShippingFeesAmount: {\n            value: 5.99,\n            currency: \"EUR\",\n          },\n          itemDefectReturnFees: \"https://schema.org/FreeReturn\",\n          restockingFee: {\n            value: 10,\n            currency: \"EUR\",\n          },\n          returnPolicySeasonalOverride: {\n            startDate: \"2025-12-01\",\n            endDate: \"2025-01-05\",\n            returnPolicyCategory:\n              \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n            merchantReturnDays: 30,\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"OnlineStore\");\n    const policy = jsonData.hasMerchantReturnPolicy;\n    expect(policy[\"@type\"]).toBe(\"MerchantReturnPolicy\");\n    expect(policy.returnShippingFeesAmount).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      value: 2.99,\n      currency: \"EUR\",\n    });\n    expect(policy.customerRemorseReturnShippingFeesAmount).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      value: 5.99,\n      currency: \"EUR\",\n    });\n    expect(policy.restockingFee).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      value: 10,\n      currency: \"EUR\",\n    });\n    expect(policy.returnPolicySeasonalOverride).toEqual({\n      \"@type\": \"MerchantReturnPolicySeasonalOverride\",\n      startDate: \"2025-12-01\",\n      endDate: \"2025-01-05\",\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 30,\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/OrganizationJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { OrganizationJsonLdProps } from \"~/types/organization.types\";\nimport {\n  processAddress,\n  processContactPoint,\n  processLogo,\n  processNumberOfEmployees,\n  processReview,\n  processAggregateRating,\n  processMerchantReturnPolicy,\n  processMemberProgram,\n} from \"~/utils/processors\";\n\nexport default function OrganizationJsonLd(props: OrganizationJsonLdProps) {\n  const {\n    type = \"Organization\",\n    scriptId,\n    scriptKey,\n    name,\n    url,\n    logo,\n    description,\n    sameAs,\n    address,\n    contactPoint,\n    telephone,\n    email,\n    alternateName,\n    foundingDate,\n    legalName,\n    taxID,\n    vatID,\n    duns,\n    leiCode,\n    naics,\n    globalLocationNumber,\n    iso6523Code,\n    numberOfEmployees,\n    review,\n    aggregateRating,\n  } = props;\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": type,\n    ...(name && { name }),\n    ...(url && { url }),\n    ...(logo && { logo: processLogo(logo) }),\n    ...(description && { description }),\n    ...(sameAs && {\n      sameAs: Array.isArray(sameAs) ? sameAs : [sameAs],\n    }),\n    ...(address && {\n      address: Array.isArray(address)\n        ? address.map(processAddress)\n        : processAddress(address),\n    }),\n    ...(contactPoint && {\n      contactPoint: Array.isArray(contactPoint)\n        ? contactPoint.map(processContactPoint)\n        : processContactPoint(contactPoint),\n    }),\n    ...(telephone && { telephone }),\n    ...(email && { email }),\n    ...(alternateName && { alternateName }),\n    ...(foundingDate && { foundingDate }),\n    ...(legalName && { legalName }),\n    ...(taxID && { taxID }),\n    ...(vatID && { vatID }),\n    ...(duns && { duns }),\n    ...(leiCode && { leiCode }),\n    ...(naics && { naics }),\n    ...(globalLocationNumber && { globalLocationNumber }),\n    ...(iso6523Code && { iso6523Code }),\n    ...(numberOfEmployees && {\n      numberOfEmployees: processNumberOfEmployees(numberOfEmployees),\n    }),\n    ...(review && {\n      review: Array.isArray(review)\n        ? review.map(processReview)\n        : processReview(review),\n    }),\n    ...(aggregateRating && {\n      aggregateRating: processAggregateRating(aggregateRating),\n    }),\n    ...(type === \"OnlineStore\" &&\n      \"hasMerchantReturnPolicy\" in props &&\n      props.hasMerchantReturnPolicy && {\n        hasMerchantReturnPolicy: Array.isArray(props.hasMerchantReturnPolicy)\n          ? props.hasMerchantReturnPolicy.map(processMerchantReturnPolicy)\n          : processMerchantReturnPolicy(props.hasMerchantReturnPolicy),\n      }),\n    ...(type === \"OnlineStore\" &&\n      \"hasMemberProgram\" in props &&\n      props.hasMemberProgram && {\n        hasMemberProgram: Array.isArray(props.hasMemberProgram)\n          ? props.hasMemberProgram.map(processMemberProgram)\n          : processMemberProgram(props.hasMemberProgram),\n      }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || `organization-jsonld-${type}`}\n    />\n  );\n}\n\nexport type { OrganizationJsonLdProps };\n"
  },
  {
    "path": "src/components/ProductJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport ProductJsonLd from \"./ProductJsonLd\";\n\ndescribe(\"ProductJsonLd\", () => {\n  it(\"renders basic Product with minimal props (with offers)\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        offers={{\n          price: 119.99,\n          priceCurrency: \"USD\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Product\",\n      name: \"Executive Anvil\",\n      offers: {\n        \"@type\": \"Offer\",\n        price: 119.99,\n        priceCurrency: \"USD\",\n      },\n    });\n  });\n\n  it(\"renders Product with review\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        review={{\n          reviewRating: {\n            ratingValue: 4,\n            bestRating: 5,\n          },\n          author: \"Fred Benson\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.review).toEqual({\n      \"@type\": \"Review\",\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 4,\n        bestRating: 5,\n      },\n      author: {\n        \"@type\": \"Person\",\n        name: \"Fred Benson\",\n      },\n    });\n  });\n\n  it(\"renders Product with aggregateRating\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        aggregateRating={{\n          ratingValue: 4.4,\n          reviewCount: 89,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.4,\n      reviewCount: 89,\n    });\n  });\n\n  it(\"handles AggregateOffer correctly\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        offers={{\n          lowPrice: 119.99,\n          highPrice: 199.99,\n          priceCurrency: \"USD\",\n          offerCount: 5,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.offers).toEqual({\n      \"@type\": \"AggregateOffer\",\n      lowPrice: 119.99,\n      highPrice: 199.99,\n      priceCurrency: \"USD\",\n      offerCount: 5,\n    });\n  });\n\n  it(\"handles review with pros and cons\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Cheese Grater Pro\"\n        review={{\n          author: \"Pascal Van Cleeff\",\n          positiveNotes: {\n            itemListElement: [\n              { name: \"Consistent results\" },\n              { name: \"Still sharp after many uses\" },\n            ],\n          },\n          negativeNotes: {\n            itemListElement: [\n              { name: \"No child protection\" },\n              { name: \"Lacking advanced features\" },\n            ],\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.review.positiveNotes).toEqual({\n      \"@type\": \"ItemList\",\n      itemListElement: [\n        {\n          \"@type\": \"ListItem\",\n          position: 1,\n          name: \"Consistent results\",\n        },\n        {\n          \"@type\": \"ListItem\",\n          position: 2,\n          name: \"Still sharp after many uses\",\n        },\n      ],\n    });\n\n    expect(jsonData.review.negativeNotes).toEqual({\n      \"@type\": \"ItemList\",\n      itemListElement: [\n        {\n          \"@type\": \"ListItem\",\n          position: 1,\n          name: \"No child protection\",\n        },\n        {\n          \"@type\": \"ListItem\",\n          position: 2,\n          name: \"Lacking advanced features\",\n        },\n      ],\n    });\n  });\n\n  it(\"handles multiple images\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        image={[\n          \"https://example.com/photos/1x1/photo.jpg\",\n          \"https://example.com/photos/4x3/photo.jpg\",\n          {\n            url: \"https://example.com/photos/16x9/photo.jpg\",\n            width: 1920,\n            height: 1080,\n          },\n        ]}\n        offers={{\n          price: 119.99,\n          priceCurrency: \"USD\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.image).toEqual([\n      \"https://example.com/photos/1x1/photo.jpg\",\n      \"https://example.com/photos/4x3/photo.jpg\",\n      {\n        \"@type\": \"ImageObject\",\n        url: \"https://example.com/photos/16x9/photo.jpg\",\n        width: 1920,\n        height: 1080,\n      },\n    ]);\n  });\n\n  it(\"handles brand as string\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        brand=\"ACME\"\n        offers={{\n          price: 119.99,\n          priceCurrency: \"USD\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.brand).toEqual({\n      \"@type\": \"Brand\",\n      name: \"ACME\",\n    });\n  });\n\n  it(\"handles brand as object\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        brand={{\n          name: \"ACME Corporation\",\n        }}\n        offers={{\n          price: 119.99,\n          priceCurrency: \"USD\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.brand).toEqual({\n      \"@type\": \"Brand\",\n      name: \"ACME Corporation\",\n    });\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        description=\"Sleeker than ACME's Classic Anvil\"\n        url=\"https://example.com/products/anvil\"\n        sku=\"0446310786\"\n        mpn=\"925872\"\n        gtin13=\"0614141999996\"\n        brand=\"ACME\"\n        category=\"Hardware\"\n        color=\"Silver\"\n        material=\"Steel\"\n        model=\"EA-2024\"\n        productID=\"anvil-001\"\n        weight={{\n          value: 10,\n          unitCode: \"KGM\",\n        }}\n        width=\"30cm\"\n        height=\"20cm\"\n        depth=\"15cm\"\n        manufacturer=\"ACME Manufacturing\"\n        releaseDate=\"2024-01-01\"\n        award=\"Best Anvil 2024\"\n        review={{\n          reviewRating: {\n            ratingValue: 4.5,\n            bestRating: 5,\n          },\n          author: \"John Doe\",\n          reviewBody: \"Great anvil!\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.4,\n          reviewCount: 89,\n        }}\n        offers={{\n          price: 119.99,\n          priceCurrency: \"USD\",\n          availability: \"InStock\",\n          url: \"https://example.com/buy/anvil\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.name).toBe(\"Executive Anvil\");\n    expect(jsonData.description).toBe(\"Sleeker than ACME's Classic Anvil\");\n    expect(jsonData.url).toBe(\"https://example.com/products/anvil\");\n    expect(jsonData.sku).toBe(\"0446310786\");\n    expect(jsonData.mpn).toBe(\"925872\");\n    expect(jsonData.gtin13).toBe(\"0614141999996\");\n    expect(jsonData.category).toBe(\"Hardware\");\n    expect(jsonData.color).toBe(\"Silver\");\n    expect(jsonData.material).toBe(\"Steel\");\n    expect(jsonData.model).toBe(\"EA-2024\");\n    expect(jsonData.productID).toBe(\"anvil-001\");\n    expect(jsonData.weight).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 10,\n      unitCode: \"KGM\",\n    });\n    expect(jsonData.width).toBe(\"30cm\");\n    expect(jsonData.height).toBe(\"20cm\");\n    expect(jsonData.depth).toBe(\"15cm\");\n    expect(jsonData.manufacturer).toEqual({\n      \"@type\": \"Person\",\n      name: \"ACME Manufacturing\",\n    });\n    expect(jsonData.releaseDate).toBe(\"2024-01-01\");\n    expect(jsonData.award).toBe(\"Best Anvil 2024\");\n  });\n\n  it(\"handles multiple reviews\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        review={[\n          {\n            reviewRating: {\n              ratingValue: 5,\n              bestRating: 5,\n            },\n            author: \"Alice\",\n            reviewBody: \"Excellent!\",\n          },\n          {\n            reviewRating: {\n              ratingValue: 4,\n              bestRating: 5,\n            },\n            author: \"Bob\",\n            reviewBody: \"Good product\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(Array.isArray(jsonData.review)).toBe(true);\n    expect(jsonData.review).toHaveLength(2);\n    expect(jsonData.review[0].author.name).toBe(\"Alice\");\n    expect(jsonData.review[1].author.name).toBe(\"Bob\");\n  });\n\n  it(\"handles array of offers\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        offers={[\n          {\n            price: 119.99,\n            priceCurrency: \"USD\",\n            seller: \"Store A\",\n          },\n          {\n            price: 129.99,\n            priceCurrency: \"USD\",\n            seller: \"Store B\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(Array.isArray(jsonData.offers)).toBe(true);\n    expect(jsonData.offers).toHaveLength(2);\n    expect(jsonData.offers[0].price).toBe(119.99);\n    expect(jsonData.offers[1].price).toBe(129.99);\n  });\n\n  it(\"handles Car type when isCar is true\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Tesla Model 3\"\n        isCar={true}\n        offers={{\n          price: 39990,\n          priceCurrency: \"USD\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toEqual([\"Product\", \"Car\"]);\n  });\n\n  it(\"handles custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        scriptId=\"custom-product-id\"\n        scriptKey=\"custom-product-key\"\n        offers={{\n          price: 119.99,\n          priceCurrency: \"USD\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n    expect(script!.id).toBe(\"custom-product-id\");\n  });\n\n  it(\"handles offer with priceSpecification\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        offers={{\n          priceSpecification: {\n            price: 119.99,\n            priceCurrency: \"USD\",\n            minPrice: 99.99,\n            maxPrice: 149.99,\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.offers.priceSpecification).toEqual({\n      \"@type\": \"PriceSpecification\",\n      price: 119.99,\n      priceCurrency: \"USD\",\n      minPrice: 99.99,\n      maxPrice: 149.99,\n    });\n  });\n\n  it(\"handles additionalProperty\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Executive Anvil\"\n        additionalProperty={[\n          {\n            \"@type\": \"PropertyValue\",\n            name: \"Warranty\",\n            value: \"2 years\",\n          },\n          {\n            \"@type\": \"PropertyValue\",\n            name: \"Assembly Required\",\n            value: \"No\",\n          },\n        ]}\n        offers={{\n          price: 119.99,\n          priceCurrency: \"USD\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.additionalProperty).toHaveLength(2);\n    expect(jsonData.additionalProperty[0].name).toBe(\"Warranty\");\n    expect(jsonData.additionalProperty[1].value).toBe(\"No\");\n  });\n\n  it(\"handles offer with merchant return policy\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Product with Return Policy\"\n        offers={{\n          price: 99.99,\n          priceCurrency: \"USD\",\n          hasMerchantReturnPolicy: {\n            applicableCountry: \"US\",\n            returnPolicyCategory:\n              \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n            merchantReturnDays: 30,\n            returnFees: \"https://schema.org/FreeReturn\",\n            refundType: \"https://schema.org/FullRefund\",\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.offers.hasMerchantReturnPolicy).toEqual({\n      \"@type\": \"MerchantReturnPolicy\",\n      applicableCountry: [\"US\"],\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 30,\n      returnFees: \"https://schema.org/FreeReturn\",\n      refundType: [\"https://schema.org/FullRefund\"],\n    });\n  });\n\n  it(\"handles offer with enhanced merchant return policy\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Product with Enhanced Return Policy\"\n        offers={{\n          price: 199.99,\n          priceCurrency: \"EUR\",\n          hasMerchantReturnPolicy: {\n            applicableCountry: [\"DE\", \"AT\"],\n            returnPolicyCountry: \"DE\",\n            returnPolicyCategory:\n              \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n            merchantReturnDays: 60,\n            returnShippingFeesAmount: {\n              value: 4.99,\n              currency: \"EUR\",\n            },\n            restockingFee: 15,\n            returnPolicySeasonalOverride: {\n              startDate: \"2025-12-01\",\n              endDate: \"2025-01-05\",\n              returnPolicyCategory:\n                \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n              merchantReturnDays: 30,\n            },\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    const policy = jsonData.offers.hasMerchantReturnPolicy;\n    expect(policy[\"@type\"]).toBe(\"MerchantReturnPolicy\");\n    expect(policy.returnShippingFeesAmount).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      value: 4.99,\n      currency: \"EUR\",\n    });\n    expect(policy.restockingFee).toBe(15);\n    expect(policy.returnPolicySeasonalOverride).toEqual({\n      \"@type\": \"MerchantReturnPolicySeasonalOverride\",\n      startDate: \"2025-12-01\",\n      endDate: \"2025-01-05\",\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 30,\n    });\n  });\n\n  it(\"handles multiple offers with different return policies\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Multi-seller Product\"\n        offers={[\n          {\n            price: 89.99,\n            priceCurrency: \"USD\",\n            seller: \"Store A\",\n            hasMerchantReturnPolicy: {\n              applicableCountry: \"US\",\n              returnPolicyCategory:\n                \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n              merchantReturnDays: 30,\n            },\n          },\n          {\n            price: 94.99,\n            priceCurrency: \"USD\",\n            seller: \"Store B\",\n            hasMerchantReturnPolicy: {\n              applicableCountry: \"US\",\n              returnPolicyCategory:\n                \"https://schema.org/MerchantReturnNotPermitted\",\n            },\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.offers).toHaveLength(2);\n    expect(jsonData.offers[0].hasMerchantReturnPolicy.merchantReturnDays).toBe(\n      30,\n    );\n    expect(\n      jsonData.offers[1].hasMerchantReturnPolicy.returnPolicyCategory,\n    ).toBe(\"https://schema.org/MerchantReturnNotPermitted\");\n  });\n});\n\ndescribe(\"ProductJsonLd with ProductGroup\", () => {\n  it(\"renders basic ProductGroup with variants\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        type=\"ProductGroup\"\n        name=\"Wool winter coat\"\n        description=\"Wool coat, new for the coming winter season\"\n        productGroupID=\"44E01\"\n        variesBy={[\"size\", \"color\"]}\n        hasVariant={[\n          {\n            name: \"Small green coat\",\n            sku: \"44E01-M11000\",\n            color: \"Green\",\n            size: \"small\",\n            offers: {\n              price: 39.99,\n              priceCurrency: \"USD\",\n              availability: \"InStock\",\n            },\n          },\n          {\n            name: \"Small light blue coat\",\n            sku: \"44E01-K11000\",\n            color: \"light blue\",\n            size: \"small\",\n            offers: {\n              price: 39.99,\n              priceCurrency: \"USD\",\n              availability: \"InStock\",\n            },\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"ProductGroup\",\n      name: \"Wool winter coat\",\n      description: \"Wool coat, new for the coming winter season\",\n      productGroupID: \"44E01\",\n      variesBy: [\"https://schema.org/size\", \"https://schema.org/color\"],\n      hasVariant: [\n        {\n          \"@type\": \"Product\",\n          name: \"Small green coat\",\n          sku: \"44E01-M11000\",\n          color: \"Green\",\n          size: \"small\",\n          offers: {\n            \"@type\": \"Offer\",\n            price: 39.99,\n            priceCurrency: \"USD\",\n            availability: \"InStock\",\n          },\n        },\n        {\n          \"@type\": \"Product\",\n          name: \"Small light blue coat\",\n          sku: \"44E01-K11000\",\n          color: \"light blue\",\n          size: \"small\",\n          offers: {\n            \"@type\": \"Offer\",\n            price: 39.99,\n            priceCurrency: \"USD\",\n            availability: \"InStock\",\n          },\n        },\n      ],\n    });\n  });\n\n  it(\"renders ProductGroup with URL-only variants\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        type=\"ProductGroup\"\n        name=\"Wool winter coat\"\n        productGroupID=\"44E01\"\n        variesBy=\"color\"\n        hasVariant={[\n          {\n            name: \"Small green coat\",\n            sku: \"44E01-M11000\",\n            color: \"Green\",\n            offers: {\n              price: 39.99,\n              priceCurrency: \"USD\",\n            },\n          },\n          { url: \"https://example.com/coat/blue\" },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.hasVariant).toHaveLength(2);\n    expect(jsonData.hasVariant[0][\"@type\"]).toBe(\"Product\");\n    expect(jsonData.hasVariant[0].name).toBe(\"Small green coat\");\n    expect(jsonData.hasVariant[1]).toEqual({\n      url: \"https://example.com/coat/blue\",\n    });\n  });\n\n  it(\"handles variesBy with full schema.org URLs\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        type=\"ProductGroup\"\n        name=\"Test Product Group\"\n        productGroupID=\"TEST01\"\n        variesBy={[\"https://schema.org/size\", \"https://schema.org/color\"]}\n        hasVariant={[]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.variesBy).toEqual([\n      \"https://schema.org/size\",\n      \"https://schema.org/color\",\n    ]);\n  });\n\n  it(\"renders ProductGroup with audience information\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        type=\"ProductGroup\"\n        name=\"Kids coat\"\n        productGroupID=\"KIDS01\"\n        audience={{\n          \"@type\": \"PeopleAudience\",\n          suggestedGender: \"unisex\",\n          suggestedAge: {\n            \"@type\": \"QuantitativeValue\",\n            minValue: 5,\n            maxValue: 12,\n            unitCode: \"ANN\",\n          },\n        }}\n        hasVariant={[]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.audience).toEqual({\n      \"@type\": \"PeopleAudience\",\n      suggestedGender: \"unisex\",\n      suggestedAge: {\n        \"@type\": \"QuantitativeValue\",\n        minValue: 5,\n        maxValue: 12,\n        unitCode: \"ANN\",\n      },\n    });\n  });\n\n  it(\"renders Product with isVariantOf reference\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Small green coat\"\n        sku=\"44E01-M11000\"\n        color=\"Green\"\n        size=\"small\"\n        isVariantOf={{ \"@id\": \"#coat_parent\" }}\n        offers={{\n          price: 39.99,\n          priceCurrency: \"USD\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Product\");\n    expect(jsonData.name).toBe(\"Small green coat\");\n    expect(jsonData.size).toBe(\"small\");\n    expect(jsonData.color).toBe(\"Green\");\n    expect(jsonData.isVariantOf).toEqual({ \"@id\": \"#coat_parent\" });\n  });\n\n  it(\"renders Product with inProductGroupWithID\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Large blue coat\"\n        sku=\"44E01-X11000\"\n        size=\"large\"\n        color=\"blue\"\n        inProductGroupWithID=\"44E01\"\n        offers={{\n          price: 49.99,\n          priceCurrency: \"USD\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.inProductGroupWithID).toBe(\"44E01\");\n    expect(jsonData.size).toBe(\"large\");\n    expect(jsonData.color).toBe(\"blue\");\n  });\n\n  it(\"auto-detects ProductGroup from hasVariant property\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        name=\"Auto-detected ProductGroup\"\n        productGroupID=\"AUTO01\"\n        hasVariant={[\n          {\n            name: \"Variant 1\",\n            sku: \"V1\",\n            offers: { price: 10, priceCurrency: \"USD\" },\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"ProductGroup\");\n    expect(jsonData.productGroupID).toBe(\"AUTO01\");\n  });\n\n  it(\"renders ProductGroup with common properties\", () => {\n    const { container } = render(\n      <ProductJsonLd\n        type=\"ProductGroup\"\n        name=\"Wool winter coat\"\n        productGroupID=\"44E01\"\n        brand=\"Good Brand\"\n        material=\"wool\"\n        pattern=\"striped\"\n        category=\"Apparel\"\n        aggregateRating={{\n          ratingValue: 4.5,\n          reviewCount: 100,\n        }}\n        hasVariant={[\n          {\n            name: \"Small coat\",\n            sku: \"SMALL\",\n            size: \"small\",\n            offers: { price: 39.99, priceCurrency: \"USD\" },\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.brand).toEqual({\n      \"@type\": \"Brand\",\n      name: \"Good Brand\",\n    });\n    expect(jsonData.material).toBe(\"wool\");\n    expect(jsonData.pattern).toBe(\"striped\");\n    expect(jsonData.category).toBe(\"Apparel\");\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.5,\n      reviewCount: 100,\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/ProductJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { ProductJsonLdProps } from \"~/types/product.types\";\nimport {\n  processImage,\n  processBrand,\n  processProductReview,\n  processAggregateRating,\n  processProductOffer,\n  processAggregateOffer,\n  processAuthor,\n  processVariesBy,\n  processProductVariant,\n  processCertification,\n  processPeopleAudience,\n  processSizeSpecification,\n  processThreeDModel,\n} from \"~/utils/processors\";\n\nexport default function ProductJsonLd(props: ProductJsonLdProps) {\n  const { scriptId, scriptKey, ...rest } = props;\n\n  // Determine if this is a ProductGroup or Product\n  const isProductGroup =\n    (\"type\" in rest && rest.type === \"ProductGroup\") ||\n    \"hasVariant\" in rest ||\n    \"variesBy\" in rest ||\n    \"productGroupID\" in rest;\n\n  let data: Record<string, unknown>;\n\n  if (isProductGroup) {\n    // Handle ProductGroup - safe to cast since we checked above\n    const groupProps = rest as Extract<\n      ProductJsonLdProps,\n      { type?: \"ProductGroup\" }\n    >;\n\n    data = {\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"ProductGroup\",\n      name: groupProps.name,\n      ...(groupProps.description && { description: groupProps.description }),\n      ...(groupProps.url && { url: groupProps.url }),\n      ...(groupProps.image && {\n        image: Array.isArray(groupProps.image)\n          ? groupProps.image.map(processImage)\n          : processImage(groupProps.image),\n      }),\n      ...(groupProps.brand && {\n        brand:\n          typeof groupProps.brand === \"string\"\n            ? { \"@type\": \"Brand\", name: groupProps.brand }\n            : processBrand(groupProps.brand),\n      }),\n      ...(groupProps.review && {\n        review: Array.isArray(groupProps.review)\n          ? groupProps.review.map(processProductReview)\n          : processProductReview(groupProps.review),\n      }),\n      ...(groupProps.aggregateRating && {\n        aggregateRating: processAggregateRating(groupProps.aggregateRating),\n      }),\n      ...(groupProps.audience && { audience: groupProps.audience }),\n      productGroupID: groupProps.productGroupID,\n      ...(groupProps.variesBy && {\n        variesBy: processVariesBy(groupProps.variesBy),\n      }),\n      ...(groupProps.hasVariant && {\n        hasVariant: groupProps.hasVariant.map(processProductVariant),\n      }),\n      ...(groupProps.pattern && { pattern: groupProps.pattern }),\n      ...(groupProps.material && { material: groupProps.material }),\n      ...(groupProps.category && { category: groupProps.category }),\n    };\n  } else {\n    // Handle regular Product - safe to cast since we checked above\n    const productProps = rest as Extract<\n      ProductJsonLdProps,\n      { type?: \"Product\" }\n    >;\n\n    // Product requires at least one of: review, aggregateRating, or offers\n    if (\n      !productProps.review &&\n      !productProps.aggregateRating &&\n      !productProps.offers &&\n      !productProps.isVariantOf\n    ) {\n      console.warn(\n        \"ProductJsonLd: Product structured data requires at least one of: review, aggregateRating, offers, or isVariantOf\",\n      );\n    }\n\n    // Process offers - can be single offer, aggregate offer, or array\n    let processedOffers;\n    if (productProps.offers) {\n      if (Array.isArray(productProps.offers)) {\n        processedOffers = productProps.offers.map((offer) => {\n          // Check if it's an AggregateOffer\n          if (\"lowPrice\" in offer && \"priceCurrency\" in offer) {\n            return processAggregateOffer(\n              offer as Parameters<typeof processAggregateOffer>[0],\n            );\n          }\n          return processProductOffer(offer);\n        });\n      } else if (\n        \"lowPrice\" in productProps.offers &&\n        \"priceCurrency\" in productProps.offers\n      ) {\n        // It's an AggregateOffer\n        processedOffers = processAggregateOffer(\n          productProps.offers as Parameters<typeof processAggregateOffer>[0],\n        );\n      } else {\n        // It's a regular Offer\n        processedOffers = processProductOffer(productProps.offers);\n      }\n    }\n\n    // Process reviews - can be single or array\n    let processedReview;\n    if (productProps.review) {\n      processedReview = Array.isArray(productProps.review)\n        ? productProps.review.map(processProductReview)\n        : processProductReview(productProps.review);\n    }\n\n    // Process brand - can be string or Brand object\n    let processedBrand;\n    if (productProps.brand) {\n      if (typeof productProps.brand === \"string\") {\n        processedBrand = {\n          \"@type\": \"Brand\",\n          name: productProps.brand,\n        };\n      } else {\n        processedBrand = processBrand(productProps.brand);\n      }\n    }\n\n    // Process weight/dimensions - can be string or QuantitativeValue\n    const processQuantitativeValue = (\n      value:\n        | string\n        | {\n            \"@type\"?: \"QuantitativeValue\";\n            value?: number;\n            unitCode?: string;\n            unitText?: string;\n          }\n        | undefined,\n    ) => {\n      if (typeof value === \"string\") {\n        return value;\n      }\n      if (value && typeof value === \"object\" && !(\"@type\" in value)) {\n        return {\n          \"@type\": \"QuantitativeValue\",\n          ...value,\n        };\n      }\n      return value;\n    };\n\n    data = {\n      \"@context\": \"https://schema.org\",\n      \"@type\": productProps.isCar ? [\"Product\", \"Car\"] : \"Product\",\n      name: productProps.name,\n      ...(productProps.description && {\n        description: productProps.description,\n      }),\n      ...(productProps.image && {\n        image: Array.isArray(productProps.image)\n          ? productProps.image.map(processImage)\n          : processImage(productProps.image),\n      }),\n      ...(productProps.sku && { sku: productProps.sku }),\n      ...(productProps.mpn && { mpn: productProps.mpn }),\n      ...(productProps.gtin && { gtin: productProps.gtin }),\n      ...(productProps.gtin8 && { gtin8: productProps.gtin8 }),\n      ...(productProps.gtin12 && { gtin12: productProps.gtin12 }),\n      ...(productProps.gtin13 && { gtin13: productProps.gtin13 }),\n      ...(productProps.gtin14 && { gtin14: productProps.gtin14 }),\n      ...(processedBrand && { brand: processedBrand }),\n      ...(processedReview && { review: processedReview }),\n      ...(productProps.aggregateRating && {\n        aggregateRating: processAggregateRating(productProps.aggregateRating),\n      }),\n      ...(processedOffers && { offers: processedOffers }),\n      ...(productProps.category && { category: productProps.category }),\n      ...(productProps.color && { color: productProps.color }),\n      ...(productProps.material && { material: productProps.material }),\n      ...(productProps.model && { model: productProps.model }),\n      ...(productProps.productID && { productID: productProps.productID }),\n      ...(productProps.url && { url: productProps.url }),\n      ...(productProps.weight && {\n        weight: processQuantitativeValue(productProps.weight),\n      }),\n      ...(productProps.width && {\n        width: processQuantitativeValue(productProps.width),\n      }),\n      ...(productProps.height && {\n        height: processQuantitativeValue(productProps.height),\n      }),\n      ...(productProps.depth && {\n        depth: processQuantitativeValue(productProps.depth),\n      }),\n      ...(productProps.additionalProperty && {\n        additionalProperty: productProps.additionalProperty,\n      }),\n      ...(productProps.manufacturer && {\n        manufacturer: processAuthor(productProps.manufacturer),\n      }),\n      ...(productProps.releaseDate && {\n        releaseDate: productProps.releaseDate,\n      }),\n      ...(productProps.productionDate && {\n        productionDate: productProps.productionDate,\n      }),\n      ...(productProps.purchaseDate && {\n        purchaseDate: productProps.purchaseDate,\n      }),\n      ...(productProps.expirationDate && {\n        expirationDate: productProps.expirationDate,\n      }),\n      ...(productProps.award && { award: productProps.award }),\n      ...(productProps.size && {\n        size: processSizeSpecification(productProps.size),\n      }),\n      ...(productProps.pattern && { pattern: productProps.pattern }),\n      ...(productProps.isbn && { isbn: productProps.isbn }),\n      ...(productProps.hasCertification && {\n        hasCertification: Array.isArray(productProps.hasCertification)\n          ? productProps.hasCertification.map(processCertification)\n          : processCertification(productProps.hasCertification),\n      }),\n      ...(productProps.audience && {\n        audience: processPeopleAudience(productProps.audience),\n      }),\n      ...(productProps.subjectOf && {\n        subjectOf: processThreeDModel(productProps.subjectOf),\n      }),\n      ...(productProps.isVariantOf && {\n        isVariantOf: productProps.isVariantOf,\n      }),\n      ...(productProps.inProductGroupWithID && {\n        inProductGroupWithID: productProps.inProductGroupWithID,\n      }),\n    };\n  }\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={\n        scriptKey || (isProductGroup ? \"productgroup-jsonld\" : \"product-jsonld\")\n      }\n    />\n  );\n}\n\nexport type { ProductJsonLdProps };\n"
  },
  {
    "path": "src/components/ProfilePageJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport ProfilePageJsonLd from \"./ProfilePageJsonLd\";\n\ndescribe(\"ProfilePageJsonLd\", () => {\n  it(\"renders basic ProfilePage with string mainEntity\", () => {\n    const { container } = render(\n      <ProfilePageJsonLd mainEntity=\"Angelo Huff\" />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"ProfilePage\",\n      mainEntity: {\n        \"@type\": \"Person\",\n        name: \"Angelo Huff\",\n      },\n    });\n  });\n\n  it(\"renders ProfilePage with Person mainEntity\", () => {\n    const { container } = render(\n      <ProfilePageJsonLd\n        mainEntity={{\n          name: \"Angelo Huff\",\n          alternateName: \"ahuff23\",\n          identifier: \"123475623\",\n          description: \"Defender of Truth\",\n          image: \"https://example.com/avatars/ahuff23.jpg\",\n          sameAs: [\n            \"https://www.example.com/real-angelo\",\n            \"https://example.com/profile/therealangelohuff\",\n          ],\n        }}\n        dateCreated=\"2024-12-23T12:34:00-05:00\"\n        dateModified=\"2024-12-26T14:53:00-05:00\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"ProfilePage\",\n      mainEntity: {\n        \"@type\": \"Person\",\n        name: \"Angelo Huff\",\n        alternateName: \"ahuff23\",\n        identifier: \"123475623\",\n        description: \"Defender of Truth\",\n        image: \"https://example.com/avatars/ahuff23.jpg\",\n        sameAs: [\n          \"https://www.example.com/real-angelo\",\n          \"https://example.com/profile/therealangelohuff\",\n        ],\n      },\n      dateCreated: \"2024-12-23T12:34:00-05:00\",\n      dateModified: \"2024-12-26T14:53:00-05:00\",\n    });\n  });\n\n  it(\"renders ProfilePage with Organization mainEntity\", () => {\n    const { container } = render(\n      <ProfilePageJsonLd\n        mainEntity={{\n          \"@type\": \"Organization\",\n          name: \"TechCorp Inc\",\n          url: \"https://techcorp.com\",\n          logo: \"https://techcorp.com/logo.png\",\n          sameAs: [\n            \"https://twitter.com/techcorp\",\n            \"https://linkedin.com/company/techcorp\",\n          ],\n          alternateName: \"Tech Corporation\",\n          identifier: \"org-123456\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"ProfilePage\",\n      mainEntity: {\n        \"@type\": \"Organization\",\n        name: \"TechCorp Inc\",\n        url: \"https://techcorp.com\",\n        logo: \"https://techcorp.com/logo.png\",\n        sameAs: [\n          \"https://twitter.com/techcorp\",\n          \"https://linkedin.com/company/techcorp\",\n        ],\n        alternateName: \"Tech Corporation\",\n        identifier: \"org-123456\",\n      },\n    });\n  });\n\n  it(\"detects Organization from properties without @type\", () => {\n    const { container } = render(\n      <ProfilePageJsonLd\n        mainEntity={{\n          name: \"Example Corp\",\n          logo: \"https://example.com/logo.png\",\n          address: \"123 Main St\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.mainEntity[\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.mainEntity.name).toBe(\"Example Corp\");\n    expect(jsonData.mainEntity.logo).toBe(\"https://example.com/logo.png\");\n  });\n\n  it(\"handles interactionStatistic array\", () => {\n    const { container } = render(\n      <ProfilePageJsonLd\n        mainEntity={{\n          name: \"Angelo Huff\",\n          interactionStatistic: [\n            {\n              interactionType: \"https://schema.org/FollowAction\",\n              userInteractionCount: 1,\n            },\n            {\n              interactionType: \"https://schema.org/LikeAction\",\n              userInteractionCount: 5,\n            },\n          ],\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.mainEntity.interactionStatistic).toEqual([\n      {\n        \"@type\": \"InteractionCounter\",\n        interactionType: \"https://schema.org/FollowAction\",\n        userInteractionCount: 1,\n      },\n      {\n        \"@type\": \"InteractionCounter\",\n        interactionType: \"https://schema.org/LikeAction\",\n        userInteractionCount: 5,\n      },\n    ]);\n  });\n\n  it(\"handles single interactionStatistic\", () => {\n    const { container } = render(\n      <ProfilePageJsonLd\n        mainEntity={{\n          name: \"Angelo Huff\",\n          interactionStatistic: {\n            interactionType: \"https://schema.org/FollowAction\",\n            userInteractionCount: 100,\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.mainEntity.interactionStatistic).toEqual({\n      \"@type\": \"InteractionCounter\",\n      interactionType: \"https://schema.org/FollowAction\",\n      userInteractionCount: 100,\n    });\n  });\n\n  it(\"handles agentInteractionStatistic\", () => {\n    const { container } = render(\n      <ProfilePageJsonLd\n        mainEntity={{\n          name: \"Angelo Huff\",\n          agentInteractionStatistic: {\n            interactionType: \"https://schema.org/WriteAction\",\n            userInteractionCount: 2346,\n          },\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.mainEntity.agentInteractionStatistic).toEqual({\n      \"@type\": \"InteractionCounter\",\n      interactionType: \"https://schema.org/WriteAction\",\n      userInteractionCount: 2346,\n    });\n  });\n\n  it(\"handles sameAs as single string\", () => {\n    const { container } = render(\n      <ProfilePageJsonLd\n        mainEntity={{\n          name: \"Angelo Huff\",\n          sameAs: \"https://example.com/profile\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.mainEntity.sameAs).toBe(\"https://example.com/profile\");\n  });\n\n  it(\"renders with custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <ProfilePageJsonLd\n        mainEntity=\"John Doe\"\n        scriptId=\"profile-script\"\n        scriptKey=\"custom-profile-key\"\n      />,\n    );\n\n    const script = container.querySelector(\"#profile-script\");\n    expect(script).toBeTruthy();\n    expect(script?.getAttribute(\"type\")).toBe(\"application/ld+json\");\n  });\n\n  it(\"renders with all properties\", () => {\n    const { container } = render(\n      <ProfilePageJsonLd\n        mainEntity={{\n          \"@type\": \"Person\",\n          name: \"Angelo Huff\",\n          alternateName: \"ahuff23\",\n          identifier: \"123475623\",\n          description: \"Defender of Truth\",\n          image: \"https://example.com/avatars/ahuff23.jpg\",\n          url: \"https://example.com/angelo\",\n          sameAs: [\n            \"https://www.example.com/real-angelo\",\n            \"https://example.com/profile/therealangelohuff\",\n          ],\n          interactionStatistic: [\n            {\n              \"@type\": \"InteractionCounter\",\n              interactionType: \"https://schema.org/FollowAction\",\n              userInteractionCount: 1,\n            },\n            {\n              \"@type\": \"InteractionCounter\",\n              interactionType: \"https://schema.org/LikeAction\",\n              userInteractionCount: 5,\n            },\n          ],\n          agentInteractionStatistic: {\n            \"@type\": \"InteractionCounter\",\n            interactionType: \"https://schema.org/WriteAction\",\n            userInteractionCount: 2346,\n          },\n        }}\n        dateCreated=\"2024-12-23T12:34:00-05:00\"\n        dateModified=\"2024-12-26T14:53:00-05:00\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"ProfilePage\",\n      mainEntity: {\n        \"@type\": \"Person\",\n        name: \"Angelo Huff\",\n        alternateName: \"ahuff23\",\n        identifier: \"123475623\",\n        description: \"Defender of Truth\",\n        image: \"https://example.com/avatars/ahuff23.jpg\",\n        url: \"https://example.com/angelo\",\n        sameAs: [\n          \"https://www.example.com/real-angelo\",\n          \"https://example.com/profile/therealangelohuff\",\n        ],\n        interactionStatistic: [\n          {\n            \"@type\": \"InteractionCounter\",\n            interactionType: \"https://schema.org/FollowAction\",\n            userInteractionCount: 1,\n          },\n          {\n            \"@type\": \"InteractionCounter\",\n            interactionType: \"https://schema.org/LikeAction\",\n            userInteractionCount: 5,\n          },\n        ],\n        agentInteractionStatistic: {\n          \"@type\": \"InteractionCounter\",\n          interactionType: \"https://schema.org/WriteAction\",\n          userInteractionCount: 2346,\n        },\n      },\n      dateCreated: \"2024-12-23T12:34:00-05:00\",\n      dateModified: \"2024-12-26T14:53:00-05:00\",\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/ProfilePageJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { ProfilePageJsonLdProps } from \"~/types/profile.types\";\nimport { processAuthor, processInteractionStatistic } from \"~/utils/processors\";\n\nexport default function ProfilePageJsonLd({\n  mainEntity,\n  dateCreated,\n  dateModified,\n  scriptId,\n  scriptKey,\n}: ProfilePageJsonLdProps) {\n  // Process mainEntity - it can be a string, Person, Organization, or objects without @type\n  const processedMainEntity = processAuthor(mainEntity);\n\n  // Further process the mainEntity to handle interactionStatistic arrays\n  if (processedMainEntity.interactionStatistic) {\n    if (Array.isArray(processedMainEntity.interactionStatistic)) {\n      processedMainEntity.interactionStatistic =\n        processedMainEntity.interactionStatistic.map(\n          processInteractionStatistic,\n        );\n    } else {\n      processedMainEntity.interactionStatistic = processInteractionStatistic(\n        processedMainEntity.interactionStatistic,\n      );\n    }\n  }\n\n  // Process agentInteractionStatistic if present\n  if (processedMainEntity.agentInteractionStatistic) {\n    processedMainEntity.agentInteractionStatistic = processInteractionStatistic(\n      processedMainEntity.agentInteractionStatistic,\n    );\n  }\n\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"ProfilePage\",\n    mainEntity: processedMainEntity,\n    ...(dateCreated && { dateCreated }),\n    ...(dateModified && { dateModified }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"profile-page-jsonld\"}\n    />\n  );\n}\n\nexport type { ProfilePageJsonLdProps };\n"
  },
  {
    "path": "src/components/QuizJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport QuizJsonLd from \"./QuizJsonLd\";\n\ndescribe(\"QuizJsonLd\", () => {\n  it(\"renders basic Quiz with minimal props\", () => {\n    const { container } = render(\n      <QuizJsonLd\n        questions={[\n          {\n            question: \"What is the capital of France?\",\n            answer: \"Paris\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org/\",\n      \"@type\": \"Quiz\",\n      hasPart: [\n        {\n          \"@type\": \"Question\",\n          eduQuestionType: \"Flashcard\",\n          text: \"What is the capital of France?\",\n          acceptedAnswer: {\n            \"@type\": \"Answer\",\n            text: \"Paris\",\n          },\n        },\n      ],\n    });\n  });\n\n  it(\"handles multiple questions\", () => {\n    const { container } = render(\n      <QuizJsonLd\n        questions={[\n          {\n            question: \"What is 2 + 2?\",\n            answer: \"4\",\n          },\n          {\n            question: \"What is the capital of Japan?\",\n            answer: \"Tokyo\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.hasPart).toHaveLength(2);\n    expect(jsonData.hasPart[0].text).toBe(\"What is 2 + 2?\");\n    expect(jsonData.hasPart[0].acceptedAnswer.text).toBe(\"4\");\n    expect(jsonData.hasPart[1].text).toBe(\"What is the capital of Japan?\");\n    expect(jsonData.hasPart[1].acceptedAnswer.text).toBe(\"Tokyo\");\n  });\n\n  it(\"handles string questions\", () => {\n    const { container } = render(\n      <QuizJsonLd questions={[\"Mitochondria is the powerhouse of the cell\"]} />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.hasPart[0]).toEqual({\n      \"@type\": \"Question\",\n      eduQuestionType: \"Flashcard\",\n      text: \"Mitochondria is the powerhouse of the cell\",\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: \"Mitochondria is the powerhouse of the cell\",\n      },\n    });\n  });\n\n  it(\"handles text/acceptedAnswer format\", () => {\n    const { container } = render(\n      <QuizJsonLd\n        questions={[\n          {\n            text: \"What is photosynthesis?\",\n            acceptedAnswer:\n              \"The process by which plants use sunlight to produce food\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.hasPart[0].text).toBe(\"What is photosynthesis?\");\n    expect(jsonData.hasPart[0].acceptedAnswer.text).toBe(\n      \"The process by which plants use sunlight to produce food\",\n    );\n  });\n\n  it(\"handles about property as string\", () => {\n    const { container } = render(\n      <QuizJsonLd\n        questions={[\n          {\n            question: \"What is DNA?\",\n            answer: \"Deoxyribonucleic acid\",\n          },\n        ]}\n        about=\"Biology\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.about).toEqual({\n      \"@type\": \"Thing\",\n      name: \"Biology\",\n    });\n  });\n\n  it(\"handles about property as Thing object\", () => {\n    const { container } = render(\n      <QuizJsonLd\n        questions={[\n          {\n            question: \"What is DNA?\",\n            answer: \"Deoxyribonucleic acid\",\n          },\n        ]}\n        about={{\n          name: \"Cell Biology\",\n          description: \"The study of cells\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.about).toEqual({\n      \"@type\": \"Thing\",\n      name: \"Cell Biology\",\n      description: \"The study of cells\",\n    });\n  });\n\n  it(\"handles educational alignment\", () => {\n    const { container } = render(\n      <QuizJsonLd\n        questions={[\n          {\n            question: \"What is the powerhouse of the cell?\",\n            answer: \"Mitochondria\",\n          },\n        ]}\n        educationalAlignment={[\n          {\n            type: \"educationalSubject\",\n            name: \"Biology\",\n          },\n          {\n            type: \"educationalLevel\",\n            name: \"Grade 9\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.educationalAlignment).toEqual([\n      {\n        \"@type\": \"AlignmentObject\",\n        alignmentType: \"educationalSubject\",\n        targetName: \"Biology\",\n      },\n      {\n        \"@type\": \"AlignmentObject\",\n        alignmentType: \"educationalLevel\",\n        targetName: \"Grade 9\",\n      },\n    ]);\n  });\n\n  it(\"handles all properties together\", () => {\n    const { container } = render(\n      <QuizJsonLd\n        questions={[\n          {\n            question: \"What is cellular respiration?\",\n            answer:\n              \"The process by which cells break down sugar to produce energy\",\n          },\n          {\n            text: \"What is ATP?\",\n            acceptedAnswer: {\n              \"@type\": \"Answer\",\n              text: \"Adenosine triphosphate - the energy currency of cells\",\n            },\n          },\n        ]}\n        about=\"Cell Energy\"\n        educationalAlignment={[\n          {\n            type: \"educationalSubject\",\n            name: \"Biology\",\n          },\n          {\n            type: \"educationalLevel\",\n            name: \"High School\",\n          },\n        ]}\n        scriptId=\"quiz-script\"\n        scriptKey=\"biology-quiz\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script?.id).toBe(\"quiz-script\");\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org/\");\n    expect(jsonData[\"@type\"]).toBe(\"Quiz\");\n    expect(jsonData.about.name).toBe(\"Cell Energy\");\n    expect(jsonData.educationalAlignment).toHaveLength(2);\n    expect(jsonData.hasPart).toHaveLength(2);\n    expect(jsonData.hasPart[1].acceptedAnswer[\"@type\"]).toBe(\"Answer\");\n  });\n\n  it(\"uses custom scriptKey\", () => {\n    const { container } = render(\n      <QuizJsonLd\n        questions={[{ question: \"Test?\", answer: \"Answer\" }]}\n        scriptKey=\"custom-quiz-key\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n    // Note: scriptKey is used internally by JsonLdScript but not visible in DOM\n  });\n\n  it(\"handles mixed question formats\", () => {\n    const { container } = render(\n      <QuizJsonLd\n        questions={[\n          \"Simple string flashcard\",\n          {\n            question: \"Question/answer format?\",\n            answer: \"Yes\",\n          },\n          {\n            text: \"Text/acceptedAnswer format?\",\n            acceptedAnswer: \"Also yes\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.hasPart).toHaveLength(3);\n    expect(jsonData.hasPart[0].text).toBe(\"Simple string flashcard\");\n    expect(jsonData.hasPart[1].text).toBe(\"Question/answer format?\");\n    expect(jsonData.hasPart[2].text).toBe(\"Text/acceptedAnswer format?\");\n  });\n});\n"
  },
  {
    "path": "src/components/QuizJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type {\n  QuizJsonLdProps,\n  QuestionInput,\n  Question,\n  Answer,\n  AlignmentObject,\n} from \"~/types/quiz.types\";\nimport type { Thing } from \"~/types/common.types\";\n\n// Process flexible question input into proper Question format\nfunction processQuestion(input: QuestionInput): Question {\n  // Handle string input - create question with the string as both question and answer\n  if (typeof input === \"string\") {\n    return {\n      \"@type\": \"Question\",\n      eduQuestionType: \"Flashcard\",\n      text: input,\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: input,\n      },\n    };\n  }\n\n  // Handle simple question/answer format\n  if (\"question\" in input && \"answer\" in input) {\n    return {\n      \"@type\": \"Question\",\n      eduQuestionType: \"Flashcard\",\n      text: input.question,\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: input.answer,\n      },\n    };\n  }\n\n  // Handle format with text/acceptedAnswer\n  if (\"text\" in input) {\n    const acceptedAnswer: Answer =\n      typeof input.acceptedAnswer === \"string\"\n        ? {\n            \"@type\": \"Answer\",\n            text: input.acceptedAnswer,\n          }\n        : input.acceptedAnswer;\n\n    return {\n      \"@type\": \"Question\",\n      eduQuestionType: \"Flashcard\",\n      text: input.text,\n      acceptedAnswer,\n    };\n  }\n\n  // Should never reach here due to TypeScript, but handle gracefully\n  return {\n    \"@type\": \"Question\",\n    eduQuestionType: \"Flashcard\",\n    text: \"\",\n    acceptedAnswer: {\n      \"@type\": \"Answer\",\n      text: \"\",\n    },\n  };\n}\n\n// Process about property\nfunction processAbout(about: string | Thing): Record<string, unknown> {\n  if (typeof about === \"string\") {\n    return {\n      \"@type\": \"Thing\",\n      name: about,\n    };\n  }\n  return {\n    \"@type\": \"Thing\",\n    ...about,\n  };\n}\n\n// Process educational alignment\nfunction processEducationalAlignment(alignment: {\n  type: \"educationalSubject\" | \"educationalLevel\";\n  name: string;\n}): AlignmentObject {\n  return {\n    \"@type\": \"AlignmentObject\",\n    alignmentType: alignment.type,\n    targetName: alignment.name,\n  };\n}\n\nexport default function QuizJsonLd({\n  questions,\n  about,\n  educationalAlignment,\n  scriptId,\n  scriptKey,\n}: QuizJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org/\",\n    \"@type\": \"Quiz\",\n    ...(about && {\n      about: processAbout(about),\n    }),\n    ...(educationalAlignment && {\n      educationalAlignment: educationalAlignment.map(\n        processEducationalAlignment,\n      ),\n    }),\n    hasPart: questions.map(processQuestion),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"quiz-jsonld\"}\n    />\n  );\n}\n\nexport type { QuizJsonLdProps };\n"
  },
  {
    "path": "src/components/RecipeJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport RecipeJsonLd from \"./RecipeJsonLd\";\n\ndescribe(\"RecipeJsonLd\", () => {\n  it(\"renders basic Recipe with minimal props\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Simple Chocolate Chip Cookies\"\n        image=\"https://example.com/cookies.jpg\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Recipe\",\n      name: \"Simple Chocolate Chip Cookies\",\n      image: \"https://example.com/cookies.jpg\",\n    });\n  });\n\n  it(\"preserves URL query parameters\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Chocolate Cookies\"\n        image=\"https://example.com/cookies.jpg\"\n        url=\"https://example.com/recipe?category=dessert&rating=5\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.url).toBe(\n      \"https://example.com/recipe?category=dessert&rating=5\",\n    );\n  });\n\n  it(\"handles string author\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Grandma's Apple Pie\"\n        image=\"https://example.com/apple-pie.jpg\"\n        author=\"Mary Johnson\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Mary Johnson\",\n    });\n  });\n\n  it(\"handles Person author object\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Chef's Special Pasta\"\n        image=\"https://example.com/pasta.jpg\"\n        author={{\n          \"@type\": \"Person\",\n          name: \"Chef Giovanni\",\n          url: \"https://example.com/chef-giovanni\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Chef Giovanni\",\n      url: \"https://example.com/chef-giovanni\",\n    });\n  });\n\n  it(\"handles Organization author\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Restaurant's Famous Burger\"\n        image=\"https://example.com/burger.jpg\"\n        author={{\n          \"@type\": \"Organization\",\n          name: \"The Gourmet Kitchen\",\n          url: \"https://example.com\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Organization\",\n      name: \"The Gourmet Kitchen\",\n      url: \"https://example.com\",\n    });\n  });\n\n  it(\"handles string image\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Test Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.image).toBe(\"https://example.com/recipe.jpg\");\n  });\n\n  it(\"handles ImageObject\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Test Recipe\"\n        image={{\n          \"@type\": \"ImageObject\",\n          url: \"https://example.com/recipe.jpg\",\n          width: 1200,\n          height: 800,\n          caption: \"Delicious recipe photo\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.image).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/recipe.jpg\",\n      width: 1200,\n      height: 800,\n      caption: \"Delicious recipe photo\",\n    });\n  });\n\n  it(\"handles multiple images\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Test Recipe\"\n        image={[\n          \"https://example.com/recipe1.jpg\",\n          {\n            \"@type\": \"ImageObject\",\n            url: \"https://example.com/recipe2.jpg\",\n            width: 800,\n            height: 600,\n          },\n          \"https://example.com/recipe3.jpg\",\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.image).toEqual([\n      \"https://example.com/recipe1.jpg\",\n      {\n        \"@type\": \"ImageObject\",\n        url: \"https://example.com/recipe2.jpg\",\n        width: 800,\n        height: 600,\n      },\n      \"https://example.com/recipe3.jpg\",\n    ]);\n  });\n\n  it(\"handles string instructions\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Simple Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        recipeInstructions=\"Mix all ingredients and bake for 30 minutes at 350°F.\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.recipeInstructions).toBe(\n      \"Mix all ingredients and bake for 30 minutes at 350°F.\",\n    );\n  });\n\n  it(\"handles HowToStep instructions\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Detailed Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        recipeInstructions={[\n          {\n            \"@type\": \"HowToStep\",\n            text: \"Preheat oven to 350°F\",\n            name: \"Preheat\",\n          },\n          {\n            \"@type\": \"HowToStep\",\n            text: \"Mix dry ingredients\",\n            url: \"https://example.com/mixing-guide\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.recipeInstructions).toEqual([\n      {\n        \"@type\": \"HowToStep\",\n        text: \"Preheat oven to 350°F\",\n        name: \"Preheat\",\n      },\n      {\n        \"@type\": \"HowToStep\",\n        text: \"Mix dry ingredients\",\n        url: \"https://example.com/mixing-guide\",\n      },\n    ]);\n  });\n\n  it(\"handles HowToSection instructions\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Complex Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        recipeInstructions={{\n          \"@type\": \"HowToSection\",\n          name: \"Preparation\",\n          itemListElement: [\n            {\n              \"@type\": \"HowToStep\",\n              text: \"Gather all ingredients\",\n            },\n            {\n              \"@type\": \"HowToStep\",\n              text: \"Prepare your workspace\",\n            },\n          ],\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.recipeInstructions).toEqual({\n      \"@type\": \"HowToSection\",\n      name: \"Preparation\",\n      itemListElement: [\n        {\n          \"@type\": \"HowToStep\",\n          text: \"Gather all ingredients\",\n        },\n        {\n          \"@type\": \"HowToStep\",\n          text: \"Prepare your workspace\",\n        },\n      ],\n    });\n  });\n\n  it(\"handles duration properties\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Timed Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        prepTime=\"PT30M\"\n        cookTime=\"PT1H\"\n        totalTime=\"PT1H30M\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.prepTime).toBe(\"PT30M\");\n    expect(jsonData.cookTime).toBe(\"PT1H\");\n    expect(jsonData.totalTime).toBe(\"PT1H30M\");\n  });\n\n  it(\"handles nutrition information\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Healthy Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        recipeYield=\"4 servings\"\n        nutrition={{\n          \"@type\": \"NutritionInformation\",\n          calories: \"250 calories\",\n          proteinContent: \"10g\",\n          carbohydrateContent: \"30g\",\n          fatContent: \"12g\",\n          saturatedFatContent: \"3g\",\n          sodiumContent: \"200mg\",\n          fiberContent: \"4g\",\n          sugarContent: \"8g\",\n          servingSize: \"1 serving\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.recipeYield).toBe(\"4 servings\");\n    expect(jsonData.nutrition).toEqual({\n      \"@type\": \"NutritionInformation\",\n      calories: \"250 calories\",\n      proteinContent: \"10g\",\n      carbohydrateContent: \"30g\",\n      fatContent: \"12g\",\n      saturatedFatContent: \"3g\",\n      sodiumContent: \"200mg\",\n      fiberContent: \"4g\",\n      sugarContent: \"8g\",\n      servingSize: \"1 serving\",\n    });\n  });\n\n  it(\"handles aggregate rating\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Popular Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        aggregateRating={{\n          \"@type\": \"AggregateRating\",\n          ratingValue: 4.5,\n          ratingCount: 120,\n          reviewCount: 98,\n          bestRating: 5,\n          worstRating: 1,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.5,\n      ratingCount: 120,\n      reviewCount: 98,\n      bestRating: 5,\n      worstRating: 1,\n    });\n  });\n\n  it(\"handles video object\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Video Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        video={{\n          \"@type\": \"VideoObject\",\n          name: \"How to Make Chocolate Chip Cookies\",\n          description: \"Step by step guide to making cookies\",\n          thumbnailUrl: \"https://example.com/video-thumb.jpg\",\n          contentUrl: \"https://example.com/cookie-video.mp4\",\n          embedUrl: \"https://example.com/embed/cookie-video\",\n          uploadDate: \"2024-01-01T08:00:00+00:00\",\n          duration: \"PT5M30S\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.video).toEqual({\n      \"@type\": \"VideoObject\",\n      name: \"How to Make Chocolate Chip Cookies\",\n      description: \"Step by step guide to making cookies\",\n      thumbnailUrl: \"https://example.com/video-thumb.jpg\",\n      contentUrl: \"https://example.com/cookie-video.mp4\",\n      embedUrl: \"https://example.com/embed/cookie-video\",\n      uploadDate: \"2024-01-01T08:00:00+00:00\",\n      duration: \"PT5M30S\",\n    });\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Complete Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        description=\"A delicious and complete recipe\"\n        author=\"Chef Mary\"\n        datePublished=\"2024-01-01T08:00:00+00:00\"\n        url=\"https://example.com/recipes/complete-recipe\"\n        prepTime=\"PT20M\"\n        cookTime=\"PT30M\"\n        totalTime=\"PT50M\"\n        recipeYield={6}\n        recipeCategory=\"dessert\"\n        recipeCuisine=\"French\"\n        keywords=\"chocolate, dessert, french cuisine\"\n        recipeIngredient={[\n          \"2 cups flour\",\n          \"1 cup sugar\",\n          \"1/2 cup butter\",\n          \"2 eggs\",\n          \"1 tsp vanilla extract\",\n        ]}\n        recipeInstructions={[\n          \"Mix dry ingredients\",\n          \"Add wet ingredients\",\n          \"Bake for 30 minutes\",\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Recipe\",\n      name: \"Complete Recipe\",\n      image: \"https://example.com/recipe.jpg\",\n      description: \"A delicious and complete recipe\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"Chef Mary\",\n      },\n      datePublished: \"2024-01-01T08:00:00+00:00\",\n      url: \"https://example.com/recipes/complete-recipe\",\n      prepTime: \"PT20M\",\n      cookTime: \"PT30M\",\n      totalTime: \"PT50M\",\n      recipeYield: 6,\n      recipeCategory: \"dessert\",\n      recipeCuisine: \"French\",\n      keywords: \"chocolate, dessert, french cuisine\",\n      recipeIngredient: [\n        \"2 cups flour\",\n        \"1 cup sugar\",\n        \"1/2 cup butter\",\n        \"2 eggs\",\n        \"1 tsp vanilla extract\",\n      ],\n      recipeInstructions: [\n        \"Mix dry ingredients\",\n        \"Add wet ingredients\",\n        \"Bake for 30 minutes\",\n      ],\n    });\n  });\n\n  it(\"handles recipeYield as string\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Test Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        recipeYield=\"4 servings\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.recipeYield).toBe(\"4 servings\");\n  });\n\n  it(\"handles recipeYield as number\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Test Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        recipeYield={8}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.recipeYield).toBe(8);\n  });\n\n  it(\"uses custom scriptId when provided\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Test Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        scriptId=\"custom-recipe-id\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script!.getAttribute(\"id\")).toBe(\"custom-recipe-id\");\n    expect(script!.getAttribute(\"data-testid\")).toBe(\"custom-recipe-id\");\n  });\n\n  it(\"uses custom scriptKey when provided\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Test Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        scriptKey=\"custom-key\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n  });\n\n  it(\"uses default scriptKey when not provided\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Test Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n  });\n\n  it(\"handles mixed instruction types\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Mixed Instructions Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        recipeInstructions={[\n          \"Preheat oven to 350°F\",\n          {\n            \"@type\": \"HowToStep\",\n            text: \"Mix all dry ingredients in a bowl\",\n            name: \"Mix ingredients\",\n          },\n          {\n            \"@type\": \"HowToSection\",\n            name: \"Baking\",\n            itemListElement: [\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Pour batter into pan\",\n              },\n              {\n                \"@type\": \"HowToStep\",\n                text: \"Bake for 25 minutes\",\n              },\n            ],\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.recipeInstructions).toEqual([\n      \"Preheat oven to 350°F\",\n      {\n        \"@type\": \"HowToStep\",\n        text: \"Mix all dry ingredients in a bowl\",\n        name: \"Mix ingredients\",\n      },\n      {\n        \"@type\": \"HowToSection\",\n        name: \"Baking\",\n        itemListElement: [\n          {\n            \"@type\": \"HowToStep\",\n            text: \"Pour batter into pan\",\n          },\n          {\n            \"@type\": \"HowToStep\",\n            text: \"Bake for 25 minutes\",\n          },\n        ],\n      },\n    ]);\n  });\n\n  it(\"handles nutrition without @type\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Test Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        recipeIngredient={[\"1 cup flour\", \"2 eggs\"]}\n        nutrition={{\n          calories: \"250 calories\",\n          servingSize: \"1 serving\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData.nutrition).toEqual({\n      \"@type\": \"NutritionInformation\",\n      calories: \"250 calories\",\n      servingSize: \"1 serving\",\n    });\n  });\n\n  it(\"handles ImageObject without @type\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Recipe with Images\"\n        image={[\n          \"https://example.com/photo1.jpg\",\n          {\n            url: \"https://example.com/photo2.jpg\",\n            width: 800,\n            height: 600,\n          },\n          {\n            \"@type\": \"ImageObject\",\n            url: \"https://example.com/photo3.jpg\",\n            width: 1200,\n            height: 900,\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.image).toHaveLength(3);\n    expect(jsonData.image[0]).toBe(\"https://example.com/photo1.jpg\");\n    expect(jsonData.image[1]).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/photo2.jpg\",\n      width: 800,\n      height: 600,\n    });\n    expect(jsonData.image[2]).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/photo3.jpg\",\n      width: 1200,\n      height: 900,\n    });\n  });\n\n  it(\"handles AggregateRating without @type\", () => {\n    const { container } = render(\n      <RecipeJsonLd\n        name=\"Rated Recipe\"\n        image=\"https://example.com/recipe.jpg\"\n        aggregateRating={{\n          ratingValue: 4.8,\n          ratingCount: 200,\n          bestRating: 5,\n          worstRating: 1,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.8,\n      ratingCount: 200,\n      bestRating: 5,\n      worstRating: 1,\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/RecipeJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { RecipeJsonLdProps } from \"~/types/recipe.types\";\nimport {\n  processAuthor,\n  processImage,\n  processNutrition,\n  processAggregateRating,\n  processInstruction,\n  processVideo,\n} from \"~/utils/processors\";\n\nexport default function RecipeJsonLd({\n  scriptId,\n  scriptKey,\n  name,\n  image,\n  description,\n  author,\n  datePublished,\n  prepTime,\n  cookTime,\n  totalTime,\n  recipeYield,\n  recipeCategory,\n  recipeCuisine,\n  nutrition,\n  recipeIngredient,\n  recipeInstructions,\n  aggregateRating,\n  video,\n  keywords,\n  url,\n}: RecipeJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Recipe\",\n    name,\n    image: Array.isArray(image) ? image.map(processImage) : processImage(image),\n    ...(description && { description }),\n    ...(author && { author: processAuthor(author) }),\n    ...(datePublished && { datePublished }),\n    ...(prepTime && { prepTime }),\n    ...(cookTime && { cookTime }),\n    ...(totalTime && { totalTime }),\n    ...(recipeYield !== undefined && { recipeYield }),\n    ...(recipeCategory && { recipeCategory }),\n    ...(recipeCuisine && { recipeCuisine }),\n    ...(nutrition && { nutrition: processNutrition(nutrition) }),\n    ...(recipeIngredient && { recipeIngredient }),\n    ...(recipeInstructions && {\n      recipeInstructions: Array.isArray(recipeInstructions)\n        ? recipeInstructions.map(processInstruction)\n        : processInstruction(recipeInstructions),\n    }),\n    ...(aggregateRating && {\n      aggregateRating: processAggregateRating(aggregateRating),\n    }),\n    ...(video && { video: processVideo(video) }),\n    ...(keywords && { keywords }),\n    ...(url && { url }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"recipe-jsonld\"}\n    />\n  );\n}\n\nexport type { RecipeJsonLdProps };\n"
  },
  {
    "path": "src/components/ReviewJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport ReviewJsonLd from \"./ReviewJsonLd\";\n\ndescribe(\"ReviewJsonLd\", () => {\n  it(\"renders minimal review with string itemReviewed\", () => {\n    const { container } = render(\n      <ReviewJsonLd\n        author=\"Bob Smith\"\n        reviewRating={{ ratingValue: 4 }}\n        itemReviewed=\"Legal Seafood\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Review\",\n      author: { \"@type\": \"Person\", name: \"Bob Smith\" },\n      reviewRating: { \"@type\": \"Rating\", ratingValue: 4 },\n      itemReviewed: { \"@type\": \"Thing\", name: \"Legal Seafood\" },\n    });\n  });\n\n  it(\"supports object itemReviewed with explicit type\", () => {\n    const { container } = render(\n      <ReviewJsonLd\n        author={{ name: \"Washington Times\" }}\n        reviewRating={{ ratingValue: 4 }}\n        itemReviewed={{ name: \"Legal Seafood\", \"@type\": \"LocalBusiness\" }}\n        reviewBody=\"Great seafood!\"\n        datePublished=\"2024-01-01\"\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.itemReviewed).toEqual({\n      \"@type\": \"LocalBusiness\",\n      name: \"Legal Seafood\",\n    });\n    expect(jsonData.reviewBody).toBe(\"Great seafood!\");\n    expect(jsonData.datePublished).toBe(\"2024-01-01\");\n  });\n\n  it(\"includes publisher and url and mainEntityOfPage\", () => {\n    const { container } = render(\n      <ReviewJsonLd\n        author=\"Jane Doe\"\n        reviewRating={{ ratingValue: 5, bestRating: 5, worstRating: 1 }}\n        itemReviewed={{ name: \"The Catcher in the Rye\", \"@type\": \"Product\" }}\n        publisher={{ name: \"ACME Reviews\" }}\n        url=\"https://example.com/review/1\"\n        mainEntityOfPage={{ \"@id\": \"https://example.com/review/1\" }}\n      />,\n    );\n\n    const jsonData = JSON.parse(\n      container.querySelector('script[type=\"application/ld+json\"]')!\n        .textContent!,\n    );\n\n    expect(jsonData.publisher).toEqual({\n      \"@type\": \"Organization\",\n      name: \"ACME Reviews\",\n    });\n    expect(jsonData.url).toBe(\"https://example.com/review/1\");\n    expect(jsonData.mainEntityOfPage).toEqual({\n      \"@type\": \"WebPage\",\n      \"@id\": \"https://example.com/review/1\",\n    });\n  });\n\n  it(\"throws when missing required fields\", () => {\n    // Missing author (compile-time invalid). Intentional for runtime test.\n    expect(() =>\n      render(\n        // @ts-expect-error - intentionally testing runtime validation for missing author\n        <ReviewJsonLd reviewRating={{ ratingValue: 4 }} itemReviewed=\"Item\" />,\n      ),\n    ).toThrow(\"Review requires an author\");\n\n    // Provide structurally invalid reviewRating via double-cast to bypass TS, trigger runtime check\n    const invalidReviewRating = {} as unknown as {\n      \"@type\": \"Rating\";\n      ratingValue: number;\n    };\n    expect(() =>\n      render(\n        <ReviewJsonLd\n          author=\"A\"\n          itemReviewed=\"Item\"\n          reviewRating={invalidReviewRating}\n        />,\n      ),\n    ).toThrow(\"Review requires reviewRating.ratingValue\");\n\n    // Provide undefined itemReviewed via double-cast to bypass TS\n    const missingItemReviewed = undefined as unknown as string;\n    expect(() =>\n      render(\n        <ReviewJsonLd\n          author=\"A\"\n          reviewRating={{ ratingValue: 1 }}\n          itemReviewed={missingItemReviewed}\n        />,\n      ),\n    ).toThrow(\"Review requires itemReviewed when used standalone\");\n  });\n});\n"
  },
  {
    "path": "src/components/ReviewJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { ReviewJsonLdProps } from \"~/types/review.types\";\nimport {\n  processAuthor,\n  processItemReviewed,\n  processMainEntityOfPage,\n  processPublisher,\n} from \"~/utils/processors\";\n\nexport default function ReviewJsonLd({\n  scriptId,\n  scriptKey,\n  author,\n  reviewRating,\n  itemReviewed,\n  datePublished,\n  reviewBody,\n  publisher,\n  url,\n  mainEntityOfPage,\n}: ReviewJsonLdProps) {\n  if (!author) {\n    throw new Error(\"Review requires an author\");\n  }\n  const ratingValue =\n    typeof reviewRating === \"object\" && reviewRating\n      ? (reviewRating as { ratingValue?: unknown }).ratingValue\n      : undefined;\n  if (ratingValue === undefined) {\n    throw new Error(\"Review requires reviewRating.ratingValue\");\n  }\n  if (!itemReviewed) {\n    throw new Error(\"Review requires itemReviewed when used standalone\");\n  }\n\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Review\",\n    author: processAuthor(author),\n    reviewRating: {\n      \"@type\": \"Rating\",\n      ...reviewRating,\n    },\n    itemReviewed: processItemReviewed(itemReviewed),\n    ...(datePublished && { datePublished }),\n    ...(reviewBody && { reviewBody }),\n    ...(publisher && { publisher: processPublisher(publisher) }),\n    ...(url && { url }),\n    ...(mainEntityOfPage && {\n      mainEntityOfPage: processMainEntityOfPage(mainEntityOfPage),\n    }),\n  } as const;\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"review-jsonld\"}\n    />\n  );\n}\n\nexport type { ReviewJsonLdProps };\n"
  },
  {
    "path": "src/components/SoftwareApplicationJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport SoftwareApplicationJsonLd from \"./SoftwareApplicationJsonLd\";\n\ndescribe(\"SoftwareApplicationJsonLd\", () => {\n  it(\"renders basic free app with minimal props\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        name=\"My Awesome App\"\n        offers={{\n          price: 0,\n          priceCurrency: \"USD\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.5,\n          ratingCount: 100,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"SoftwareApplication\",\n      name: \"My Awesome App\",\n      offers: {\n        \"@type\": \"Offer\",\n        price: 0,\n        priceCurrency: \"USD\",\n      },\n      aggregateRating: {\n        \"@type\": \"AggregateRating\",\n        ratingValue: 4.5,\n        ratingCount: 100,\n      },\n    });\n  });\n\n  it(\"renders paid app with pricing\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        name=\"Premium App\"\n        offers={{\n          price: 9.99,\n          priceCurrency: \"USD\",\n        }}\n        review={{\n          author: \"John Doe\",\n          reviewRating: {\n            ratingValue: 5,\n          },\n          reviewBody: \"Excellent app!\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.offers).toEqual({\n      \"@type\": \"Offer\",\n      price: 9.99,\n      priceCurrency: \"USD\",\n    });\n    expect(jsonData.review).toEqual({\n      \"@type\": \"Review\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"John Doe\",\n      },\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 5,\n      },\n      reviewBody: \"Excellent app!\",\n    });\n  });\n\n  it(\"renders MobileApplication type when specified\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        type=\"MobileApplication\"\n        name=\"Mobile App\"\n        operatingSystem=\"Android 5.0+\"\n        offers={{\n          price: 0,\n          priceCurrency: \"USD\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.2,\n          reviewCount: 50,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"MobileApplication\");\n    expect(jsonData.operatingSystem).toBe(\"Android 5.0+\");\n  });\n\n  it(\"renders WebApplication type\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        type=\"WebApplication\"\n        name=\"Web Tool\"\n        url=\"https://example.com/app\"\n        offers={{\n          price: 0,\n          priceCurrency: \"USD\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.8,\n          bestRating: 5,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"WebApplication\");\n    expect(jsonData.url).toBe(\"https://example.com/app\");\n  });\n\n  it(\"handles VideoGame co-typed with MobileApplication\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        type={[\"VideoGame\", \"MobileApplication\"]}\n        name=\"Mobile Game\"\n        applicationCategory=\"GameApplication\"\n        offers={{\n          price: 2.99,\n          priceCurrency: \"USD\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.6,\n          ratingCount: 1000,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toEqual([\"VideoGame\", \"MobileApplication\"]);\n    expect(jsonData.applicationCategory).toBe(\"GameApplication\");\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        type=\"BusinessApplication\"\n        name=\"Enterprise Software\"\n        description=\"Comprehensive business management solution\"\n        url=\"https://example.com\"\n        image=\"https://example.com/logo.png\"\n        applicationCategory=\"BusinessApplication\"\n        applicationSubCategory=\"ProjectManagement\"\n        applicationSuite=\"Microsoft Office\"\n        operatingSystem=\"Windows 10, macOS 10.15+\"\n        memoryRequirements=\"8GB RAM\"\n        processorRequirements=\"Intel Core i5 or equivalent\"\n        storageRequirements=\"2GB\"\n        availableOnDevice=\"Desktop\"\n        downloadUrl=\"https://example.com/download\"\n        installUrl=\"https://example.com/install\"\n        countriesSupported={[\"US\", \"CA\", \"GB\"]}\n        countriesNotSupported=\"CN\"\n        permissions={[\"storage\", \"camera\", \"microphone\"]}\n        softwareVersion=\"2.5.1\"\n        releaseNotes=\"Bug fixes and performance improvements\"\n        screenshot={[\n          \"https://example.com/screenshot1.jpg\",\n          {\n            url: \"https://example.com/screenshot2.jpg\",\n            caption: \"Dashboard view\",\n          },\n        ]}\n        featureList={[\n          \"Real-time collaboration\",\n          \"Advanced analytics\",\n          \"Cloud sync\",\n        ]}\n        offers={{\n          price: 49.99,\n          priceCurrency: \"USD\",\n          availability: \"https://schema.org/InStock\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.7,\n          ratingCount: 500,\n          reviewCount: 450,\n        }}\n        author=\"TechCorp Inc.\"\n        publisher={{\n          name: \"TechCorp Publishing\",\n          url: \"https://techcorp.com\",\n        }}\n        datePublished=\"2024-01-15\"\n        dateModified=\"2024-03-20\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData).toMatchObject({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"BusinessApplication\",\n      name: \"Enterprise Software\",\n      description: \"Comprehensive business management solution\",\n      url: \"https://example.com\",\n      image: \"https://example.com/logo.png\",\n      applicationCategory: \"BusinessApplication\",\n      applicationSubCategory: \"ProjectManagement\",\n      applicationSuite: \"Microsoft Office\",\n      operatingSystem: \"Windows 10, macOS 10.15+\",\n      memoryRequirements: \"8GB RAM\",\n      processorRequirements: \"Intel Core i5 or equivalent\",\n      storageRequirements: \"2GB\",\n      availableOnDevice: \"Desktop\",\n      downloadUrl: \"https://example.com/download\",\n      installUrl: \"https://example.com/install\",\n      countriesSupported: [\"US\", \"CA\", \"GB\"],\n      countriesNotSupported: \"CN\",\n      permissions: [\"storage\", \"camera\", \"microphone\"],\n      softwareVersion: \"2.5.1\",\n      releaseNotes: \"Bug fixes and performance improvements\",\n      screenshot: [\n        \"https://example.com/screenshot1.jpg\",\n        {\n          \"@type\": \"ImageObject\",\n          url: \"https://example.com/screenshot2.jpg\",\n          caption: \"Dashboard view\",\n        },\n      ],\n      featureList: [\n        \"Real-time collaboration\",\n        \"Advanced analytics\",\n        \"Cloud sync\",\n      ],\n      offers: {\n        \"@type\": \"Offer\",\n        price: 49.99,\n        priceCurrency: \"USD\",\n        availability: \"https://schema.org/InStock\",\n      },\n      aggregateRating: {\n        \"@type\": \"AggregateRating\",\n        ratingValue: 4.7,\n        ratingCount: 500,\n        reviewCount: 450,\n      },\n      author: {\n        \"@type\": \"Organization\",\n        name: \"TechCorp Inc.\",\n      },\n      publisher: {\n        \"@type\": \"Organization\",\n        name: \"TechCorp Publishing\",\n        url: \"https://techcorp.com\",\n      },\n      datePublished: \"2024-01-15\",\n      dateModified: \"2024-03-20\",\n    });\n  });\n\n  it(\"handles string permissions\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        name=\"App with String Permissions\"\n        permissions=\"location, storage\"\n        offers={{ price: 0, priceCurrency: \"USD\" }}\n        aggregateRating={{ ratingValue: 4 }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.permissions).toBe(\"location, storage\");\n  });\n\n  it(\"handles string featureList\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        name=\"App with String Features\"\n        featureList=\"Feature 1, Feature 2, Feature 3\"\n        offers={{ price: 0, priceCurrency: \"USD\" }}\n        aggregateRating={{ ratingValue: 4 }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.featureList).toBe(\"Feature 1, Feature 2, Feature 3\");\n  });\n\n  it(\"handles multiple offers\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        name=\"Multi-tier App\"\n        offers={[\n          {\n            price: 0,\n            priceCurrency: \"USD\",\n            availability: \"https://schema.org/InStock\",\n          },\n          {\n            price: 9.99,\n            priceCurrency: \"USD\",\n            availability: \"https://schema.org/InStock\",\n          },\n        ]}\n        aggregateRating={{ ratingValue: 4.3 }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.offers).toHaveLength(2);\n    expect(jsonData.offers[0][\"@type\"]).toBe(\"Offer\");\n    expect(jsonData.offers[1][\"@type\"]).toBe(\"Offer\");\n    expect(jsonData.offers[0].price).toBe(0);\n    expect(jsonData.offers[1].price).toBe(9.99);\n  });\n\n  it(\"handles multiple reviews\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        name=\"Well-reviewed App\"\n        offers={{ price: 0, priceCurrency: \"USD\" }}\n        review={[\n          {\n            author: \"Alice\",\n            reviewRating: { ratingValue: 5 },\n            reviewBody: \"Excellent!\",\n          },\n          {\n            author: \"Bob\",\n            reviewRating: { ratingValue: 4 },\n            reviewBody: \"Very good\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.review).toHaveLength(2);\n    expect(jsonData.review[0].author.name).toBe(\"Alice\");\n    expect(jsonData.review[1].author.name).toBe(\"Bob\");\n  });\n\n  it(\"defaults dateModified to datePublished when not provided\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        name=\"App with publish date\"\n        datePublished=\"2024-01-01\"\n        offers={{ price: 0, priceCurrency: \"USD\" }}\n        aggregateRating={{ ratingValue: 4 }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.datePublished).toBe(\"2024-01-01\");\n    expect(jsonData.dateModified).toBe(\"2024-01-01\");\n  });\n\n  it(\"uses custom scriptId and scriptKey when provided\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        name=\"Custom ID App\"\n        scriptId=\"my-app-jsonld\"\n        scriptKey=\"custom-key\"\n        offers={{ price: 0, priceCurrency: \"USD\" }}\n        aggregateRating={{ ratingValue: 4 }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script?.getAttribute(\"id\")).toBe(\"my-app-jsonld\");\n  });\n\n  it(\"handles string countriesSupported\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        name=\"Regional App\"\n        countriesSupported=\"US\"\n        offers={{ price: 0, priceCurrency: \"USD\" }}\n        aggregateRating={{ ratingValue: 4 }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.countriesSupported).toBe(\"US\");\n  });\n\n  it(\"handles EducationalApplication type\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        type=\"EducationalApplication\"\n        name=\"Learning App\"\n        applicationCategory=\"Education\"\n        offers={{ price: 0, priceCurrency: \"USD\" }}\n        aggregateRating={{ ratingValue: 4.8, ratingCount: 2000 }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"EducationalApplication\");\n    expect(jsonData.applicationCategory).toBe(\"Education\");\n  });\n\n  it(\"handles image as ImageObject\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        name=\"App with Image Object\"\n        image={{\n          url: \"https://example.com/logo.png\",\n          width: 512,\n          height: 512,\n        }}\n        offers={{ price: 0, priceCurrency: \"USD\" }}\n        aggregateRating={{ ratingValue: 4 }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.image).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/logo.png\",\n      width: 512,\n      height: 512,\n    });\n  });\n\n  it(\"handles array of images\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        name=\"App with Multiple Images\"\n        image={[\n          \"https://example.com/logo1.png\",\n          {\n            url: \"https://example.com/logo2.png\",\n            caption: \"Alternative logo\",\n          },\n        ]}\n        offers={{ price: 0, priceCurrency: \"USD\" }}\n        aggregateRating={{ ratingValue: 4 }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.image).toEqual([\n      \"https://example.com/logo1.png\",\n      {\n        \"@type\": \"ImageObject\",\n        url: \"https://example.com/logo2.png\",\n        caption: \"Alternative logo\",\n      },\n    ]);\n  });\n\n  it(\"handles organization publisher with logo\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        name=\"App with Publisher\"\n        publisher={{\n          name: \"Tech Publisher\",\n          logo: {\n            url: \"https://example.com/publisher-logo.png\",\n            width: 600,\n            height: 60,\n          },\n        }}\n        offers={{ price: 0, priceCurrency: \"USD\" }}\n        aggregateRating={{ ratingValue: 4 }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.publisher).toMatchObject({\n      \"@type\": \"Organization\",\n      name: \"Tech Publisher\",\n      logo: {\n        \"@type\": \"ImageObject\",\n        url: \"https://example.com/publisher-logo.png\",\n        width: 600,\n        height: 60,\n      },\n    });\n  });\n\n  it(\"handles contentRating for VideoGame co-typed with MobileApplication\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        type={[\"VideoGame\", \"MobileApplication\"]}\n        name=\"Mobile Adventure Game\"\n        applicationCategory=\"GameApplication\"\n        operatingSystem=\"iOS 13.0+, Android 9.0+\"\n        contentRating=\"Everyone 10+\"\n        offers={{\n          price: 4.99,\n          priceCurrency: \"USD\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.6,\n          ratingCount: 10000,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toEqual([\"VideoGame\", \"MobileApplication\"]);\n    expect(jsonData.contentRating).toBe(\"Everyone 10+\");\n    expect(jsonData.operatingSystem).toBe(\"iOS 13.0+, Android 9.0+\");\n  });\n\n  it(\"handles contentRating for MobileApplication\", () => {\n    const { container } = render(\n      <SoftwareApplicationJsonLd\n        type=\"MobileApplication\"\n        name=\"Educational Mobile App\"\n        applicationCategory=\"EducationalApplication\"\n        contentRating=\"Teen\"\n        offers={{\n          price: 0,\n          priceCurrency: \"USD\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.7,\n          ratingCount: 5000,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@type\"]).toBe(\"MobileApplication\");\n    expect(jsonData.contentRating).toBe(\"Teen\");\n  });\n});\n"
  },
  {
    "path": "src/components/SoftwareApplicationJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type {\n  SoftwareApplicationJsonLdProps,\n  ApplicationType,\n  VideoGameCoTyped,\n} from \"~/types/softwareApplication.types\";\nimport {\n  processAuthor,\n  processImage,\n  processPublisher,\n  processAggregateRating,\n  processReview,\n  processOffer,\n  processScreenshot,\n  processFeatureList,\n} from \"~/utils/processors\";\n\nexport default function SoftwareApplicationJsonLd({\n  type = \"SoftwareApplication\",\n  scriptId,\n  scriptKey,\n  name,\n  description,\n  url,\n  image,\n  applicationCategory,\n  applicationSubCategory,\n  applicationSuite,\n  operatingSystem,\n  memoryRequirements,\n  processorRequirements,\n  storageRequirements,\n  availableOnDevice,\n  downloadUrl,\n  installUrl,\n  countriesSupported,\n  countriesNotSupported,\n  permissions,\n  softwareVersion,\n  releaseNotes,\n  screenshot,\n  featureList,\n  offers,\n  aggregateRating,\n  review,\n  author,\n  publisher,\n  datePublished,\n  dateModified,\n  contentRating,\n}: SoftwareApplicationJsonLdProps) {\n  // Determine the actual @type to use\n  let schemaType: ApplicationType | VideoGameCoTyped;\n  if (Array.isArray(type)) {\n    // VideoGame co-typed with another application type\n    schemaType = type;\n  } else {\n    schemaType = type;\n  }\n\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": schemaType,\n    // Required properties\n    ...(name && { name }),\n    // Conditionally include properties\n    ...(description && { description }),\n    ...(url && { url }),\n    ...(image && {\n      image: Array.isArray(image)\n        ? image.map(processImage)\n        : processImage(image),\n    }),\n    ...(applicationCategory && { applicationCategory }),\n    ...(applicationSubCategory && { applicationSubCategory }),\n    ...(applicationSuite && { applicationSuite }),\n    ...(operatingSystem && { operatingSystem }),\n    ...(memoryRequirements && { memoryRequirements }),\n    ...(processorRequirements && { processorRequirements }),\n    ...(storageRequirements && { storageRequirements }),\n    ...(availableOnDevice && { availableOnDevice }),\n    ...(downloadUrl && { downloadUrl }),\n    ...(installUrl && { installUrl }),\n    ...(countriesSupported && {\n      countriesSupported: Array.isArray(countriesSupported)\n        ? countriesSupported\n        : countriesSupported,\n    }),\n    ...(countriesNotSupported && {\n      countriesNotSupported: Array.isArray(countriesNotSupported)\n        ? countriesNotSupported\n        : countriesNotSupported,\n    }),\n    ...(permissions && {\n      permissions: processFeatureList(permissions),\n    }),\n    ...(softwareVersion && { softwareVersion }),\n    ...(releaseNotes && { releaseNotes }),\n    ...(screenshot && {\n      screenshot: Array.isArray(screenshot)\n        ? screenshot.map(processScreenshot)\n        : processScreenshot(screenshot),\n    }),\n    ...(featureList && {\n      featureList: processFeatureList(featureList),\n    }),\n    ...(offers && {\n      offers: Array.isArray(offers)\n        ? offers.map(processOffer)\n        : processOffer(offers),\n    }),\n    ...(aggregateRating && {\n      aggregateRating: processAggregateRating(aggregateRating),\n    }),\n    ...(review && {\n      review: Array.isArray(review)\n        ? review.map(processReview)\n        : processReview(review),\n    }),\n    ...(author && {\n      author: processAuthor(author),\n    }),\n    ...(publisher && {\n      publisher: processPublisher(publisher),\n    }),\n    ...(datePublished && { datePublished }),\n    ...(dateModified && { dateModified }),\n    // Apply defaults where appropriate\n    ...(!dateModified && datePublished && { dateModified: datePublished }),\n    ...(contentRating && { contentRating }),\n  };\n\n  // Generate a unique key based on the type\n  const typeKey = Array.isArray(schemaType)\n    ? schemaType.join(\"-\").toLowerCase()\n    : schemaType.toLowerCase();\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || `software-application-jsonld-${typeKey}`}\n    />\n  );\n}\n\nexport type { SoftwareApplicationJsonLdProps };\n"
  },
  {
    "path": "src/components/VacationRentalJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport VacationRentalJsonLd from \"./VacationRentalJsonLd\";\n\ndescribe(\"VacationRentalJsonLd\", () => {\n  it(\"renders basic VacationRental with minimal required props\", () => {\n    const { container } = render(\n      <VacationRentalJsonLd\n        containsPlace={{\n          occupancy: {\n            value: 5,\n          },\n        }}\n        identifier=\"abc123\"\n        image=\"https://example.com/image1.jpg\"\n        latitude={42.12345}\n        longitude={101.12345}\n        name=\"My Beautiful Vacation Rental\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"VacationRental\",\n      containsPlace: {\n        \"@type\": \"Accommodation\",\n        occupancy: {\n          \"@type\": \"QuantitativeValue\",\n          value: 5,\n        },\n      },\n      identifier: \"abc123\",\n      image: [\"https://example.com/image1.jpg\"],\n      latitude: 42.12345,\n      longitude: 101.12345,\n      name: \"My Beautiful Vacation Rental\",\n    });\n  });\n\n  it(\"handles array of images\", () => {\n    const { container } = render(\n      <VacationRentalJsonLd\n        containsPlace={{\n          occupancy: {\n            value: 2,\n          },\n        }}\n        identifier=\"xyz789\"\n        image={[\n          \"https://example.com/image1.jpg\",\n          \"https://example.com/image2.jpg\",\n          \"https://example.com/image3.jpg\",\n          \"https://example.com/image4.jpg\",\n          \"https://example.com/image5.jpg\",\n          \"https://example.com/image6.jpg\",\n          \"https://example.com/image7.jpg\",\n          \"https://example.com/image8.jpg\",\n        ]}\n        latitude=\"42.12345\"\n        longitude=\"101.12345\"\n        name=\"Beach House Rental\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.image).toHaveLength(8);\n    expect(jsonData.image[0]).toBe(\"https://example.com/image1.jpg\");\n    expect(jsonData.image[7]).toBe(\"https://example.com/image8.jpg\");\n  });\n\n  it(\"handles ImageObject for images\", () => {\n    const { container } = render(\n      <VacationRentalJsonLd\n        containsPlace={{\n          occupancy: {\n            value: 4,\n          },\n        }}\n        identifier=\"test123\"\n        image={{\n          url: \"https://example.com/hero.jpg\",\n          width: 1920,\n          height: 1080,\n          caption: \"Beautiful vacation rental exterior\",\n        }}\n        latitude={42.12345}\n        longitude={101.12345}\n        name=\"Mountain Cabin\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.image).toEqual([\n      {\n        \"@type\": \"ImageObject\",\n        url: \"https://example.com/hero.jpg\",\n        width: 1920,\n        height: 1080,\n        caption: \"Beautiful vacation rental exterior\",\n      },\n    ]);\n  });\n\n  it(\"handles comprehensive accommodation details\", () => {\n    const { container } = render(\n      <VacationRentalJsonLd\n        containsPlace={{\n          additionalType: \"EntirePlace\",\n          bed: [\n            {\n              numberOfBeds: 1,\n              typeOfBed: \"Queen\",\n            },\n            {\n              numberOfBeds: 2,\n              typeOfBed: \"Single\",\n            },\n          ],\n          occupancy: {\n            value: 5,\n          },\n          amenityFeature: [\n            {\n              name: \"ac\",\n              value: true,\n            },\n            {\n              name: \"wifi\",\n              value: true,\n            },\n            {\n              name: \"poolType\",\n              value: \"Outdoor\",\n            },\n          ],\n          floorSize: {\n            value: 75,\n            unitCode: \"MTK\",\n          },\n          numberOfBathroomsTotal: 2.5,\n          numberOfBedrooms: 3,\n          numberOfRooms: 5,\n          petsAllowed: true,\n          smokingAllowed: false,\n        }}\n        identifier=\"lux123\"\n        image=\"https://example.com/luxury.jpg\"\n        latitude={42.12345}\n        longitude={101.12345}\n        name=\"Luxury Villa\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.containsPlace).toEqual({\n      \"@type\": \"Accommodation\",\n      additionalType: \"EntirePlace\",\n      bed: [\n        {\n          \"@type\": \"BedDetails\",\n          numberOfBeds: 1,\n          typeOfBed: \"Queen\",\n        },\n        {\n          \"@type\": \"BedDetails\",\n          numberOfBeds: 2,\n          typeOfBed: \"Single\",\n        },\n      ],\n      occupancy: {\n        \"@type\": \"QuantitativeValue\",\n        value: 5,\n      },\n      amenityFeature: [\n        {\n          \"@type\": \"LocationFeatureSpecification\",\n          name: \"ac\",\n          value: true,\n        },\n        {\n          \"@type\": \"LocationFeatureSpecification\",\n          name: \"wifi\",\n          value: true,\n        },\n        {\n          \"@type\": \"LocationFeatureSpecification\",\n          name: \"poolType\",\n          value: \"Outdoor\",\n        },\n      ],\n      floorSize: {\n        \"@type\": \"QuantitativeValue\",\n        value: 75,\n        unitCode: \"MTK\",\n      },\n      numberOfBathroomsTotal: 2.5,\n      numberOfBedrooms: 3,\n      numberOfRooms: 5,\n      petsAllowed: true,\n      smokingAllowed: false,\n    });\n  });\n\n  it(\"handles all optional properties\", () => {\n    const { container } = render(\n      <VacationRentalJsonLd\n        containsPlace={{\n          occupancy: {\n            value: 6,\n          },\n        }}\n        identifier=\"full123\"\n        image=\"https://example.com/rental.jpg\"\n        latitude={42.12345}\n        longitude={101.12345}\n        name=\"Complete Vacation Rental\"\n        additionalType=\"Villa\"\n        address={{\n          addressCountry: \"US\",\n          addressLocality: \"Mountain View\",\n          addressRegion: \"California\",\n          postalCode: \"94043\",\n          streetAddress: \"1600 Amphitheatre Pkwy, Unit 6E\",\n        }}\n        aggregateRating={{\n          ratingValue: 4.5,\n          ratingCount: 10,\n          reviewCount: 3,\n          bestRating: 5,\n        }}\n        brand={{\n          name: \"Luxury Rentals Inc\",\n        }}\n        checkinTime=\"18:00:00+08:00\"\n        checkoutTime=\"11:00:00+08:00\"\n        description=\"A great Vacation Rental in the perfect neighborhood.\"\n        knowsLanguage={[\"en-US\", \"fr-FR\", \"es-ES\"]}\n        review={[\n          {\n            reviewRating: {\n              ratingValue: 5,\n              bestRating: 5,\n            },\n            author: \"John Doe\",\n            datePublished: \"2024-01-01\",\n          },\n          {\n            reviewRating: {\n              ratingValue: 4,\n              bestRating: 5,\n            },\n            author: {\n              name: \"Jane Smith\",\n            },\n            datePublished: \"2024-01-15\",\n            reviewBody: \"Great place to stay!\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.additionalType).toBe(\"Villa\");\n    expect(jsonData.address).toEqual({\n      \"@type\": \"PostalAddress\",\n      addressCountry: \"US\",\n      addressLocality: \"Mountain View\",\n      addressRegion: \"California\",\n      postalCode: \"94043\",\n      streetAddress: \"1600 Amphitheatre Pkwy, Unit 6E\",\n    });\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.5,\n      ratingCount: 10,\n      reviewCount: 3,\n      bestRating: 5,\n    });\n    expect(jsonData.brand).toEqual({\n      \"@type\": \"Brand\",\n      name: \"Luxury Rentals Inc\",\n    });\n    expect(jsonData.checkinTime).toBe(\"18:00:00+08:00\");\n    expect(jsonData.checkoutTime).toBe(\"11:00:00+08:00\");\n    expect(jsonData.description).toBe(\n      \"A great Vacation Rental in the perfect neighborhood.\",\n    );\n    expect(jsonData.knowsLanguage).toEqual([\"en-US\", \"fr-FR\", \"es-ES\"]);\n    expect(jsonData.review).toHaveLength(2);\n  });\n\n  it(\"handles string latitude and longitude\", () => {\n    const { container } = render(\n      <VacationRentalJsonLd\n        containsPlace={{\n          occupancy: {\n            value: 3,\n          },\n        }}\n        identifier=\"str123\"\n        image=\"https://example.com/rental.jpg\"\n        latitude=\"42.12345\"\n        longitude=\"101.12345\"\n        name=\"String Coordinates Rental\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.latitude).toBe(\"42.12345\");\n    expect(jsonData.longitude).toBe(\"101.12345\");\n  });\n\n  it(\"prefers geo coordinates over latitude/longitude\", () => {\n    const { container } = render(\n      <VacationRentalJsonLd\n        containsPlace={{\n          occupancy: {\n            value: 3,\n          },\n        }}\n        identifier=\"geo123\"\n        image=\"https://example.com/rental.jpg\"\n        latitude={42.12345}\n        longitude={101.12345}\n        geo={{\n          latitude: 43.54321,\n          longitude: 102.54321,\n        }}\n        name=\"Geo Coordinates Rental\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.latitude).toBe(43.54321);\n    expect(jsonData.longitude).toBe(102.54321);\n  });\n\n  it(\"handles single language string\", () => {\n    const { container } = render(\n      <VacationRentalJsonLd\n        containsPlace={{\n          occupancy: {\n            value: 4,\n          },\n        }}\n        identifier=\"lang123\"\n        image=\"https://example.com/rental.jpg\"\n        latitude={42.12345}\n        longitude={101.12345}\n        name=\"Single Language Rental\"\n        knowsLanguage=\"en-US\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.knowsLanguage).toEqual([\"en-US\"]);\n  });\n\n  it(\"handles custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <VacationRentalJsonLd\n        containsPlace={{\n          occupancy: {\n            value: 2,\n          },\n        }}\n        identifier=\"custom123\"\n        image=\"https://example.com/rental.jpg\"\n        latitude={42.12345}\n        longitude={101.12345}\n        name=\"Custom Script Rental\"\n        scriptId=\"custom-vacation-rental\"\n        scriptKey=\"vacation-rental-key\"\n      />,\n    );\n\n    const script = container.querySelector(\"#custom-vacation-rental\");\n    expect(script).toBeTruthy();\n    expect(script?.getAttribute(\"data-testid\")).toBe(\"custom-vacation-rental\");\n  });\n\n  it(\"handles amenityFeature without @type\", () => {\n    const { container } = render(\n      <VacationRentalJsonLd\n        containsPlace={{\n          occupancy: {\n            value: 4,\n          },\n          amenityFeature: {\n            name: \"beachAccess\",\n            value: true,\n          },\n        }}\n        identifier=\"amenity123\"\n        image=\"https://example.com/rental.jpg\"\n        latitude={42.12345}\n        longitude={101.12345}\n        name=\"Beach Access Rental\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.containsPlace.amenityFeature).toEqual({\n      \"@type\": \"LocationFeatureSpecification\",\n      name: \"beachAccess\",\n      value: true,\n    });\n  });\n\n  it(\"handles bed without @type\", () => {\n    const { container } = render(\n      <VacationRentalJsonLd\n        containsPlace={{\n          occupancy: {\n            value: 2,\n          },\n          bed: {\n            numberOfBeds: 1,\n            typeOfBed: \"King\",\n          },\n        }}\n        identifier=\"bed123\"\n        image=\"https://example.com/rental.jpg\"\n        latitude={42.12345}\n        longitude={101.12345}\n        name=\"King Bed Rental\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.containsPlace.bed).toEqual({\n      \"@type\": \"BedDetails\",\n      numberOfBeds: 1,\n      typeOfBed: \"King\",\n    });\n  });\n\n  it(\"handles review with string author\", () => {\n    const { container } = render(\n      <VacationRentalJsonLd\n        containsPlace={{\n          occupancy: {\n            value: 4,\n          },\n        }}\n        identifier=\"review123\"\n        image=\"https://example.com/rental.jpg\"\n        latitude={42.12345}\n        longitude={101.12345}\n        name=\"Reviewed Rental\"\n        review={{\n          reviewRating: {\n            ratingValue: 5,\n          },\n          author: \"Happy Customer\",\n          datePublished: \"2024-01-01\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.review).toEqual({\n      \"@type\": \"Review\",\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 5,\n      },\n      author: {\n        \"@type\": \"Person\",\n        name: \"Happy Customer\",\n      },\n      datePublished: \"2024-01-01\",\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/VacationRentalJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { VacationRentalJsonLdProps } from \"~/types/vacationrental.types\";\nimport {\n  processImage,\n  processAddress,\n  processAggregateRating,\n  processBrand,\n  processAccommodation,\n  processReview,\n} from \"~/utils/processors\";\n\nexport default function VacationRentalJsonLd({\n  scriptId,\n  scriptKey,\n  containsPlace,\n  identifier,\n  image,\n  latitude,\n  longitude,\n  name,\n  additionalType,\n  address,\n  aggregateRating,\n  brand,\n  checkinTime,\n  checkoutTime,\n  description,\n  knowsLanguage,\n  review,\n  geo,\n}: VacationRentalJsonLdProps) {\n  // Process images - minimum 8 required\n  const processedImages = Array.isArray(image)\n    ? image.map(processImage)\n    : [processImage(image)];\n\n  // Use geo coordinates if provided, otherwise use latitude/longitude\n  const finalLatitude = geo?.latitude ?? latitude;\n  const finalLongitude = geo?.longitude ?? longitude;\n\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"VacationRental\",\n    containsPlace: processAccommodation(containsPlace),\n    identifier,\n    image: processedImages,\n    latitude: finalLatitude,\n    longitude: finalLongitude,\n    name,\n    ...(additionalType && { additionalType }),\n    ...(address && { address: processAddress(address) }),\n    ...(aggregateRating && {\n      aggregateRating: processAggregateRating(aggregateRating),\n    }),\n    ...(brand && { brand: processBrand(brand) }),\n    ...(checkinTime && { checkinTime }),\n    ...(checkoutTime && { checkoutTime }),\n    ...(description && { description }),\n    ...(knowsLanguage && {\n      knowsLanguage: Array.isArray(knowsLanguage)\n        ? knowsLanguage\n        : [knowsLanguage],\n    }),\n    ...(review && {\n      review: Array.isArray(review)\n        ? review.map(processReview)\n        : processReview(review),\n    }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || \"vacationrental-jsonld\"}\n    />\n  );\n}\n\nexport type { VacationRentalJsonLdProps };\n"
  },
  {
    "path": "src/components/VideoJsonLd.test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport VideoJsonLd from \"./VideoJsonLd\";\n\ndescribe(\"VideoJsonLd\", () => {\n  it(\"renders basic VideoObject with minimal props\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script).toBeTruthy();\n\n    const jsonData = JSON.parse(script!.textContent!);\n    expect(jsonData).toEqual({\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"VideoObject\",\n      name: \"Test Video\",\n      description: \"This is a test video\",\n      thumbnailUrl: \"https://example.com/thumbnail.jpg\",\n      uploadDate: \"2024-01-01T00:00:00.000Z\",\n    });\n  });\n\n  it(\"handles multiple thumbnail URLs\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl={[\n          \"https://example.com/thumbnail-1x1.jpg\",\n          \"https://example.com/thumbnail-4x3.jpg\",\n          \"https://example.com/thumbnail-16x9.jpg\",\n        ]}\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.thumbnailUrl).toEqual([\n      \"https://example.com/thumbnail-1x1.jpg\",\n      \"https://example.com/thumbnail-4x3.jpg\",\n      \"https://example.com/thumbnail-16x9.jpg\",\n    ]);\n  });\n\n  it(\"handles ImageObject thumbnails\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl={{\n          url: \"https://example.com/thumbnail.jpg\",\n          width: 1920,\n          height: 1080,\n        }}\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.thumbnailUrl).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/thumbnail.jpg\",\n      width: 1920,\n      height: 1080,\n    });\n  });\n\n  it(\"handles content and embed URLs\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        contentUrl=\"https://example.com/video.mp4\"\n        embedUrl=\"https://example.com/embed/video\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.contentUrl).toBe(\"https://example.com/video.mp4\");\n    expect(jsonData.embedUrl).toBe(\"https://example.com/embed/video\");\n  });\n\n  it(\"handles duration and expiration\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        duration=\"PT30M\"\n        expires=\"2025-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.duration).toBe(\"PT30M\");\n    expect(jsonData.expires).toBe(\"2025-01-01T00:00:00.000Z\");\n  });\n\n  it(\"handles interaction statistics\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        interactionStatistic={{\n          interactionType: \"WatchAction\",\n          userInteractionCount: 1000,\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.interactionStatistic).toEqual({\n      \"@type\": \"InteractionCounter\",\n      interactionType: \"WatchAction\",\n      userInteractionCount: 1000,\n    });\n  });\n\n  it(\"handles multiple interaction statistics\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        interactionStatistic={[\n          {\n            interactionType: \"WatchAction\",\n            userInteractionCount: 1000,\n          },\n          {\n            interactionType: \"LikeAction\",\n            userInteractionCount: 50,\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.interactionStatistic).toEqual([\n      {\n        \"@type\": \"InteractionCounter\",\n        interactionType: \"WatchAction\",\n        userInteractionCount: 1000,\n      },\n      {\n        \"@type\": \"InteractionCounter\",\n        interactionType: \"LikeAction\",\n        userInteractionCount: 50,\n      },\n    ]);\n  });\n\n  it(\"handles regions allowed and ineligible\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        regionsAllowed={[\"US\", \"CA\"]}\n        ineligibleRegion=\"CN\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.regionsAllowed).toEqual([\"US\", \"CA\"]);\n    expect(jsonData.ineligibleRegion).toEqual([\"CN\"]);\n  });\n\n  it(\"handles single region as string\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        regionsAllowed=\"US\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.regionsAllowed).toEqual([\"US\"]);\n  });\n\n  it(\"handles BroadcastEvent for live videos\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Live Video\"\n        description=\"This is a live video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        publication={{\n          isLiveBroadcast: true,\n          startDate: \"2024-10-27T14:00:00+00:00\",\n          endDate: \"2024-10-27T16:00:00+00:00\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.publication).toEqual({\n      \"@type\": \"BroadcastEvent\",\n      isLiveBroadcast: true,\n      startDate: \"2024-10-27T14:00:00+00:00\",\n      endDate: \"2024-10-27T16:00:00+00:00\",\n    });\n  });\n\n  it(\"handles multiple BroadcastEvents\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Live Video\"\n        description=\"This is a live video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        publication={[\n          {\n            isLiveBroadcast: true,\n            startDate: \"2024-10-27T14:00:00+00:00\",\n            endDate: \"2024-10-27T16:00:00+00:00\",\n          },\n          {\n            isLiveBroadcast: true,\n            startDate: \"2024-10-27T18:00:00+00:00\",\n            endDate: \"2024-10-27T20:00:00+00:00\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.publication).toHaveLength(2);\n    expect(jsonData.publication[0][\"@type\"]).toBe(\"BroadcastEvent\");\n    expect(jsonData.publication[1][\"@type\"]).toBe(\"BroadcastEvent\");\n  });\n\n  it(\"handles Clip for key moments\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Video with Chapters\"\n        description=\"This video has key moments\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        hasPart={{\n          name: \"Introduction\",\n          startOffset: 0,\n          endOffset: 30,\n          url: \"https://example.com/video?t=0\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.hasPart).toEqual({\n      \"@type\": \"Clip\",\n      name: \"Introduction\",\n      startOffset: 0,\n      endOffset: 30,\n      url: \"https://example.com/video?t=0\",\n    });\n  });\n\n  it(\"handles multiple Clips\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Video with Chapters\"\n        description=\"This video has key moments\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        hasPart={[\n          {\n            name: \"Introduction\",\n            startOffset: 0,\n            endOffset: 30,\n            url: \"https://example.com/video?t=0\",\n          },\n          {\n            name: \"Main Content\",\n            startOffset: 30,\n            endOffset: 300,\n            url: \"https://example.com/video?t=30\",\n          },\n        ]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.hasPart).toHaveLength(2);\n    expect(jsonData.hasPart[0][\"@type\"]).toBe(\"Clip\");\n    expect(jsonData.hasPart[1][\"@type\"]).toBe(\"Clip\");\n  });\n\n  it(\"handles SeekToAction for automatic key moments\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Video with SeekToAction\"\n        description=\"This video supports automatic key moments\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        potentialAction={{\n          target: \"https://example.com/video?t={seek_to_second_number}\",\n          \"startOffset-input\": \"required name=seek_to_second_number\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.potentialAction).toEqual({\n      \"@type\": \"SeekToAction\",\n      target: \"https://example.com/video?t={seek_to_second_number}\",\n      \"startOffset-input\": \"required name=seek_to_second_number\",\n    });\n  });\n\n  it(\"handles author as string\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        author=\"John Doe\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n    });\n  });\n\n  it(\"handles author as object\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        author={{\n          name: \"John Doe\",\n          url: \"https://example.com/john\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n      url: \"https://example.com/john\",\n    });\n  });\n\n  it(\"handles multiple authors\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        author={[\"John Doe\", \"Jane Smith\"]}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.author).toEqual([\n      {\n        \"@type\": \"Person\",\n        name: \"John Doe\",\n      },\n      {\n        \"@type\": \"Person\",\n        name: \"Jane Smith\",\n      },\n    ]);\n  });\n\n  it(\"handles publisher\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n        publisher={{\n          name: \"Video Company\",\n          logo: \"https://example.com/logo.png\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData.publisher).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Video Company\",\n      logo: \"https://example.com/logo.png\",\n    });\n  });\n\n  it(\"handles custom scriptId and scriptKey\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        scriptId=\"custom-video-id\"\n        scriptKey=\"custom-video-key\"\n        name=\"Test Video\"\n        description=\"This is a test video\"\n        thumbnailUrl=\"https://example.com/thumbnail.jpg\"\n        uploadDate=\"2024-01-01T00:00:00.000Z\"\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    expect(script?.id).toBe(\"custom-video-id\");\n  });\n\n  it(\"handles all properties combined\", () => {\n    const { container } = render(\n      <VideoJsonLd\n        type=\"VideoObject\"\n        name=\"Complete Video Example\"\n        description=\"This video has all properties\"\n        thumbnailUrl={[\n          \"https://example.com/thumb-1x1.jpg\",\n          \"https://example.com/thumb-16x9.jpg\",\n        ]}\n        uploadDate=\"2024-01-01T08:00:00+00:00\"\n        contentUrl=\"https://example.com/video.mp4\"\n        embedUrl=\"https://example.com/embed/video\"\n        duration=\"PT1H30M\"\n        expires=\"2025-01-01T00:00:00+00:00\"\n        interactionStatistic={{\n          interactionType: \"WatchAction\",\n          userInteractionCount: 5000,\n        }}\n        regionsAllowed={[\"US\", \"CA\", \"GB\"]}\n        ineligibleRegion={[\"CN\", \"RU\"]}\n        publication={{\n          isLiveBroadcast: true,\n          startDate: \"2024-12-25T20:00:00+00:00\",\n          endDate: \"2024-12-25T22:00:00+00:00\",\n        }}\n        hasPart={[\n          {\n            name: \"Opening\",\n            startOffset: 0,\n            endOffset: 120,\n            url: \"https://example.com/video?t=0\",\n          },\n          {\n            name: \"Main Event\",\n            startOffset: 120,\n            endOffset: 3600,\n            url: \"https://example.com/video?t=120\",\n          },\n        ]}\n        potentialAction={{\n          target: \"https://example.com/video?t={seek_to_second_number}\",\n          \"startOffset-input\": \"required name=seek_to_second_number\",\n        }}\n        author=\"Video Creator\"\n        publisher={{\n          name: \"Video Platform Inc.\",\n          logo: \"https://example.com/logo.png\",\n          url: \"https://example.com\",\n        }}\n      />,\n    );\n\n    const script = container.querySelector(\n      'script[type=\"application/ld+json\"]',\n    );\n    const jsonData = JSON.parse(script!.textContent!);\n\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"VideoObject\");\n    expect(jsonData.name).toBe(\"Complete Video Example\");\n    expect(jsonData.thumbnailUrl).toHaveLength(2);\n    expect(jsonData.publication[\"@type\"]).toBe(\"BroadcastEvent\");\n    expect(jsonData.hasPart).toHaveLength(2);\n    expect(jsonData.potentialAction[\"@type\"]).toBe(\"SeekToAction\");\n    expect(jsonData.author[\"@type\"]).toBe(\"Person\");\n    expect(jsonData.publisher[\"@type\"]).toBe(\"Organization\");\n  });\n});\n"
  },
  {
    "path": "src/components/VideoJsonLd.tsx",
    "content": "import { JsonLdScript } from \"~/core/JsonLdScript\";\nimport type { VideoJsonLdProps } from \"~/types/video.types\";\nimport {\n  processAuthor,\n  processImage,\n  processInteractionStatistic,\n  processPublisher,\n  processBroadcastEvent,\n  processClip,\n  processSeekToAction,\n} from \"~/utils/processors\";\n\nexport default function VideoJsonLd({\n  type = \"VideoObject\",\n  scriptId,\n  scriptKey,\n  name,\n  description,\n  thumbnailUrl,\n  uploadDate,\n  contentUrl,\n  embedUrl,\n  duration,\n  expires,\n  interactionStatistic,\n  regionsAllowed,\n  ineligibleRegion,\n  publication,\n  hasPart,\n  potentialAction,\n  author,\n  publisher,\n}: VideoJsonLdProps) {\n  const data = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": type,\n    name,\n    description,\n    thumbnailUrl: Array.isArray(thumbnailUrl)\n      ? thumbnailUrl.map(processImage)\n      : processImage(thumbnailUrl),\n    uploadDate,\n    ...(contentUrl && { contentUrl }),\n    ...(embedUrl && { embedUrl }),\n    ...(duration && { duration }),\n    ...(expires && { expires }),\n    ...(interactionStatistic && {\n      interactionStatistic: Array.isArray(interactionStatistic)\n        ? interactionStatistic.map(processInteractionStatistic)\n        : processInteractionStatistic(interactionStatistic),\n    }),\n    ...(regionsAllowed && {\n      regionsAllowed: Array.isArray(regionsAllowed)\n        ? regionsAllowed\n        : [regionsAllowed],\n    }),\n    ...(ineligibleRegion && {\n      ineligibleRegion: Array.isArray(ineligibleRegion)\n        ? ineligibleRegion\n        : [ineligibleRegion],\n    }),\n    ...(publication && {\n      publication: Array.isArray(publication)\n        ? publication.map(processBroadcastEvent)\n        : processBroadcastEvent(publication),\n    }),\n    ...(hasPart && {\n      hasPart: Array.isArray(hasPart)\n        ? hasPart.map(processClip)\n        : processClip(hasPart),\n    }),\n    ...(potentialAction && {\n      potentialAction: processSeekToAction(potentialAction),\n    }),\n    ...(author && {\n      author: Array.isArray(author)\n        ? author.map(processAuthor)\n        : processAuthor(author),\n    }),\n    ...(publisher && { publisher: processPublisher(publisher) }),\n  };\n\n  return (\n    <JsonLdScript\n      data={data}\n      id={scriptId}\n      scriptKey={scriptKey || `video-jsonld-${type}`}\n    />\n  );\n}\n\nexport type { VideoJsonLdProps };\n"
  },
  {
    "path": "src/core/JsonLdScript.test.tsx",
    "content": "import { render, screen } from \"@testing-library/react\";\nimport { describe, it, expect } from \"vitest\";\nimport { JsonLdScript } from \"./JsonLdScript\";\n\ndescribe(\"JsonLdScript\", () => {\n  const testData = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Thing\",\n    name: \"Test Thing\",\n    description: \"This is a test description.\",\n  };\n  const scriptKey = \"test-jsonld-thing\";\n  const scriptId = \"jsonld-thing-script\";\n\n  it('should render a script tag with type \"application/ld+json\"', () => {\n    render(\n      <JsonLdScript data={testData} scriptKey={scriptKey} id={scriptId} />,\n    );\n    const scriptElement = screen.getByTestId(scriptId);\n    expect(scriptElement.tagName).toBe(\"SCRIPT\");\n    expect(scriptElement).toHaveAttribute(\"type\", \"application/ld+json\");\n  });\n\n  it(\"should correctly stringify the data prop into the script content\", () => {\n    render(\n      <JsonLdScript data={testData} scriptKey={scriptKey} id={scriptId} />,\n    );\n    const scriptElement = screen.getByTestId(scriptId);\n    // Note: Vitest/JSDOM might not execute the script, so we check innerHTML.\n    // The actual JSON.stringify in the component will handle escaping.\n    expect(scriptElement.innerHTML).toBe(JSON.stringify(testData));\n  });\n\n  it(\"should use the provided id as the script tag id and data-testid\", () => {\n    render(\n      <JsonLdScript data={testData} scriptKey={scriptKey} id={scriptId} />,\n    );\n    const scriptElement = screen.getByTestId(scriptId);\n    expect(scriptElement).toHaveAttribute(\"id\", scriptId);\n  });\n\n  it(\"should use the scriptKey for the React key (not directly testable in output, but good practice)\", () => {\n    // This test is more about ensuring the prop is passed.\n    // React keys are for React's internal reconciliation and don't appear in the DOM.\n    // We can't directly assert the key, but we can ensure the component renders.\n    const { container } = render(\n      <JsonLdScript data={testData} scriptKey={scriptKey} id={scriptId} />,\n    );\n    expect(container.firstChild).toBeInTheDocument();\n  });\n\n  it(\"should return null if no data is provided\", () => {\n    const { container } = render(\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      <JsonLdScript data={null as any} scriptKey={scriptKey} id={scriptId} />,\n    );\n    expect(container.firstChild).toBeNull();\n  });\n\n  it(\"should render an empty script if data is an empty object (though practically this might be filtered)\", () => {\n    // Depending on future logic, an empty object might also result in null.\n    // For the current stub, it will render.\n    const emptyData = {};\n    render(\n      <JsonLdScript data={emptyData} scriptKey={scriptKey} id={scriptId} />,\n    );\n    const scriptElement = screen.getByTestId(scriptId);\n    expect(scriptElement.innerHTML).toBe(JSON.stringify(emptyData));\n  });\n});\n"
  },
  {
    "path": "src/core/JsonLdScript.tsx",
    "content": "import { stringify } from \"~/utils/stringify\";\n\ninterface JsonLdScriptProps<T = Record<string, unknown>> {\n  data: T;\n  id?: string;\n  scriptKey: string; // For React key\n}\n\nexport function JsonLdScript<T = Record<string, unknown>>({\n  data,\n  id,\n  scriptKey,\n}: JsonLdScriptProps<T>): React.JSX.Element | null {\n  if (data === null || data === undefined) {\n    // Explicitly check for null/undefined\n    return null;\n  }\n\n  const jsonString = stringify(data);\n\n  return (\n    <script\n      type=\"application/ld+json\"\n      id={id || scriptKey}\n      data-testid={id}\n      dangerouslySetInnerHTML={{ __html: jsonString }}\n      key={scriptKey}\n    />\n  );\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "// Core utilities\nexport { JsonLdScript } from \"./core/JsonLdScript\";\n\n// Processors for custom component creation\nexport * as processors from \"./utils/processors.export\";\n\n// Common types for custom component creation\nexport * from \"./types/common.types\";\n\n// JSON-LD Components\nexport {\n  default as ArticleJsonLd,\n  type ArticleJsonLdProps,\n} from \"./components/ArticleJsonLd\";\nexport {\n  default as ClaimReviewJsonLd,\n  type ClaimReviewJsonLdProps,\n} from \"./components/ClaimReviewJsonLd\";\nexport {\n  default as CreativeWorkJsonLd,\n  type CreativeWorkJsonLdProps,\n} from \"./components/CreativeWorkJsonLd\";\nexport {\n  default as RecipeJsonLd,\n  type RecipeJsonLdProps,\n} from \"./components/RecipeJsonLd\";\nexport {\n  default as HowToJsonLd,\n  type HowToJsonLdProps,\n} from \"./components/HowToJsonLd\";\nexport {\n  default as OrganizationJsonLd,\n  type OrganizationJsonLdProps,\n} from \"./components/OrganizationJsonLd\";\nexport {\n  default as LocalBusinessJsonLd,\n  type LocalBusinessJsonLdProps,\n} from \"./components/LocalBusinessJsonLd\";\nexport {\n  default as MerchantReturnPolicyJsonLd,\n  type MerchantReturnPolicyJsonLdProps,\n} from \"./components/MerchantReturnPolicyJsonLd\";\nexport {\n  default as MovieCarouselJsonLd,\n  type MovieCarouselJsonLdProps,\n} from \"./components/MovieCarouselJsonLd\";\nexport {\n  default as BreadcrumbJsonLd,\n  type BreadcrumbJsonLdProps,\n} from \"./components/BreadcrumbJsonLd\";\nexport {\n  default as CarouselJsonLd,\n  type CarouselJsonLdProps,\n} from \"./components/CarouselJsonLd\";\nexport {\n  default as CourseJsonLd,\n  type CourseJsonLdProps,\n} from \"./components/CourseJsonLd\";\nexport {\n  default as EventJsonLd,\n  type EventJsonLdProps,\n} from \"./components/EventJsonLd\";\nexport {\n  default as FAQJsonLd,\n  type FAQJsonLdProps,\n} from \"./components/FAQJsonLd\";\nexport {\n  default as ImageJsonLd,\n  type ImageJsonLdProps,\n} from \"./components/ImageJsonLd\";\nexport {\n  default as QuizJsonLd,\n  type QuizJsonLdProps,\n} from \"./components/QuizJsonLd\";\nexport {\n  default as DatasetJsonLd,\n  type DatasetJsonLdProps,\n} from \"./components/DatasetJsonLd\";\nexport {\n  default as JobPostingJsonLd,\n  type JobPostingJsonLdProps,\n} from \"./components/JobPostingJsonLd\";\nexport {\n  default as DiscussionForumPostingJsonLd,\n  type DiscussionForumPostingJsonLdProps,\n} from \"./components/DiscussionForumPostingJsonLd\";\nexport {\n  default as EmployerAggregateRatingJsonLd,\n  type EmployerAggregateRatingJsonLdProps,\n} from \"./components/EmployerAggregateRatingJsonLd\";\nexport {\n  default as VacationRentalJsonLd,\n  type VacationRentalJsonLdProps,\n} from \"./components/VacationRentalJsonLd\";\nexport {\n  default as VideoJsonLd,\n  type VideoJsonLdProps,\n} from \"./components/VideoJsonLd\";\nexport {\n  default as ProfilePageJsonLd,\n  type ProfilePageJsonLdProps,\n} from \"./components/ProfilePageJsonLd\";\nexport {\n  default as SoftwareApplicationJsonLd,\n  type SoftwareApplicationJsonLdProps,\n} from \"./components/SoftwareApplicationJsonLd\";\nexport {\n  default as ProductJsonLd,\n  type ProductJsonLdProps,\n} from \"./components/ProductJsonLd\";\nexport {\n  default as ReviewJsonLd,\n  type ReviewJsonLdProps,\n} from \"./components/ReviewJsonLd\";\nexport {\n  default as AggregateRatingJsonLd,\n  type AggregateRatingJsonLdProps,\n} from \"./components/AggregateRatingJsonLd\";\n"
  },
  {
    "path": "src/pages/README.md",
    "content": "# Next SEO - Pages Router Support\n\nThis directory contains SEO generation functions for Next.js Pages Router applications.\n\n> **Note:** These functions are specifically for Pages Router. For App Router applications, use the main next-seo package exports instead.\n\n## Installation\n\n```bash\nnpm install next-seo\n# or\nyarn add next-seo\n# or\npnpm add next-seo\n```\n\n## Usage\n\nImport from `next-seo/pages` instead of `next-seo`:\n\n```tsx\nimport { generateNextSeo, generateDefaultSeo } from \"next-seo/pages\";\n```\n\n> **Important:** In Pages Router, you need to wrap the generated SEO tags with Next.js's `<Head>` component to ensure the meta tags are properly rendered in the document head.\n\n### generateNextSeo Function\n\nAdd SEO meta tags to individual pages:\n\n```tsx\n// pages/about.tsx\nimport Head from \"next/head\";\nimport { generateNextSeo } from \"next-seo/pages\";\n\nexport default function AboutPage() {\n  return (\n    <>\n      <Head>\n        {generateNextSeo({\n          title: \"About Us\",\n          description: \"Learn more about our company\",\n          canonical: \"https://example.com/about\",\n          openGraph: {\n            url: \"https://example.com/about\",\n            title: \"About Us\",\n            description: \"Learn more about our company\",\n            images: [\n              {\n                url: \"https://example.com/og-image.jpg\",\n                width: 800,\n                height: 600,\n                alt: \"About Us\",\n              },\n            ],\n          },\n        })}\n      </Head>\n      <h1>About Us</h1>\n      {/* Page content */}\n    </>\n  );\n}\n```\n\n### generateDefaultSeo Function\n\nSet global SEO defaults in your `_app.tsx`:\n\n```tsx\n// pages/_app.tsx\nimport type { AppProps } from \"next/app\";\nimport Head from \"next/head\";\nimport { generateDefaultSeo } from \"next-seo/pages\";\n\nconst DEFAULT_SEO = {\n  titleTemplate: \"MySite | %s\",\n  defaultTitle: \"MySite\",\n  description: \"Welcome to MySite\",\n  openGraph: {\n    type: \"website\",\n    locale: \"en_US\",\n    url: \"https://example.com/\",\n    siteName: \"MySite\",\n  },\n  twitter: {\n    handle: \"@mysite\",\n    site: \"@mysite\",\n    cardType: \"summary_large_image\",\n  },\n};\n\nexport default function MyApp({ Component, pageProps }: AppProps) {\n  return (\n    <>\n      <Head>{generateDefaultSeo(DEFAULT_SEO)}</Head>\n      <Component {...pageProps} />\n    </>\n  );\n}\n```\n\n## Available Props\n\n### Common Props\n\n- `title` - Page title\n- `titleTemplate` - Title template for all pages (use `%s` for title placeholder)\n- `defaultTitle` - Default title when no title is set\n- `description` - Page description\n- `canonical` - Canonical URL\n- `themeColor` - Theme color meta tag\n- `noindex` - Set to `true` to noindex the page\n- `nofollow` - Set to `true` to nofollow the page\n- `robotsProps` - Additional robots meta properties\n\n### OpenGraph Props\n\n```tsx\nopenGraph={{\n  type: 'website',\n  url: 'https://example.com',\n  title: 'Open Graph Title',\n  description: 'Open Graph Description',\n  images: [{\n    url: 'https://example.com/og.jpg',\n    width: 800,\n    height: 600,\n    alt: 'Og Image Alt',\n  }],\n  siteName: 'SiteName',\n  locale: 'en_US',\n}}\n```\n\n### Twitter Props\n\n```tsx\ntwitter={{\n  handle: '@handle',\n  site: '@site',\n  cardType: 'summary_large_image',\n}}\n```\n\n### Additional Meta Tags\n\n```tsx\nadditionalMetaTags={[\n  {\n    property: 'dc:creator',\n    content: 'Jane Doe'\n  },\n  {\n    name: 'application-name',\n    content: 'NextSeo'\n  }\n]}\n```\n\n### Additional Link Tags\n\n```tsx\nadditionalLinkTags={[\n  {\n    rel: 'icon',\n    href: 'https://example.com/favicon.ico',\n  },\n  {\n    rel: 'apple-touch-icon',\n    href: 'https://example.com/apple-touch-icon.png',\n    sizes: '76x76'\n  }\n]}\n```\n\n## Migration from Legacy Import\n\nIf you're using the old import path or component-based approach, update your code:\n\n```tsx\n// Old (deprecated components)\nimport { NextSeo, DefaultSeo } from \"next-seo\";\n// Usage: <NextSeo title=\"...\" />\n\n// New (Pages Router functions)\nimport { generateNextSeo, generateDefaultSeo } from \"next-seo/pages\";\n// Usage: {generateNextSeo({ title: \"...\" })}\n```\n\n## TypeScript Support\n\nAll types are exported from `next-seo/pages`:\n\n```tsx\nimport type { NextSeoProps, DefaultSeoProps } from \"next-seo/pages\";\n```\n\n## More Information\n\nFor complete documentation and advanced usage, please refer to the main [next-seo documentation](https://github.com/garmeeh/next-seo).\n"
  },
  {
    "path": "src/pages/core/__snapshots__/buildTags.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`buildTags and generateSeoTags > Article SEO renders correctly 1`] = `\n[\n  <title>\n    Next SEO | Article Page Title\n  </title>,\n  <meta\n    content=\"index,follow\"\n    name=\"robots\"\n  />,\n  <meta\n    content=\"Description of article page\"\n    name=\"description\"\n  />,\n  <meta\n    content=\"summary_large_image\"\n    name=\"twitter:card\"\n  />,\n  <meta\n    content=\"@site\"\n    name=\"twitter:site\"\n  />,\n  <meta\n    content=\"@handle\"\n    name=\"twitter:creator\"\n  />,\n  <meta\n    content=\"Open Graph Article Title\"\n    property=\"og:title\"\n  />,\n  <meta\n    content=\"Description of open graph article\"\n    property=\"og:description\"\n  />,\n  <meta\n    content=\"https://www.example.com/articles/article-title\"\n    property=\"og:url\"\n  />,\n  <meta\n    content=\"article\"\n    property=\"og:type\"\n  />,\n  <meta\n    content=\"2017-06-21T23:04:13Z\"\n    property=\"article:published_time\"\n  />,\n  <meta\n    content=\"2018-01-21T18:04:43Z\"\n    property=\"article:modified_time\"\n  />,\n  <meta\n    content=\"2022-12-21T22:04:11Z\"\n    property=\"article:expiration_time\"\n  />,\n  <meta\n    content=\"https://www.example.com/authors/@firstnameA-lastnameA\"\n    property=\"article:author\"\n  />,\n  <meta\n    content=\"https://www.example.com/authors/@firstnameB-lastnameB\"\n    property=\"article:author\"\n  />,\n  <meta\n    content=\"Section II\"\n    property=\"article:section\"\n  />,\n  <meta\n    content=\"Tag A\"\n    property=\"article:tag\"\n  />,\n  <meta\n    content=\"Tag B\"\n    property=\"article:tag\"\n  />,\n  <meta\n    content=\"https://www.test.ie/og-image-article-title-01.jpg\"\n    property=\"og:image\"\n  />,\n  <meta\n    content=\"Og Image Alt Article Title A\"\n    property=\"og:image:alt\"\n  />,\n  <meta\n    content=\"850\"\n    property=\"og:image:width\"\n  />,\n  <meta\n    content=\"650\"\n    property=\"og:image:height\"\n  />,\n  <meta\n    content=\"https://www.test.ie/og-image-article-title-02.jpg\"\n    property=\"og:image\"\n  />,\n  <meta\n    content=\"Og Image Alt Article Title B\"\n    property=\"og:image:alt\"\n  />,\n  <meta\n    content=\"950\"\n    property=\"og:image:width\"\n  />,\n  <meta\n    content=\"850\"\n    property=\"og:image:height\"\n  />,\n  <meta\n    content=\"SiteName\"\n    property=\"og:site_name\"\n  />,\n]\n`;\n\nexports[`buildTags and generateSeoTags > Book SEO renders correctly 1`] = `\n[\n  <title>\n    Next SEO | Book Page Title\n  </title>,\n  <meta\n    content=\"index,follow\"\n    name=\"robots\"\n  />,\n  <meta\n    content=\"Description of book page\"\n    name=\"description\"\n  />,\n  <meta\n    content=\"summary_large_image\"\n    name=\"twitter:card\"\n  />,\n  <meta\n    content=\"@site\"\n    name=\"twitter:site\"\n  />,\n  <meta\n    content=\"@handle\"\n    name=\"twitter:creator\"\n  />,\n  <meta\n    content=\"Open Graph Book Title\"\n    property=\"og:title\"\n  />,\n  <meta\n    content=\"Description of open graph book\"\n    property=\"og:description\"\n  />,\n  <meta\n    content=\"https://www.example.com/books/book-title\"\n    property=\"og:url\"\n  />,\n  <meta\n    content=\"book\"\n    property=\"og:type\"\n  />,\n  <meta\n    content=\"https://www.example.com/authors/@firstnameA-lastnameA\"\n    property=\"book:author\"\n  />,\n  <meta\n    content=\"https://www.example.com/authors/@firstnameB-lastnameB\"\n    property=\"book:author\"\n  />,\n  <meta\n    content=\"978-3-16-148410-0\"\n    property=\"book:isbn\"\n  />,\n  <meta\n    content=\"2018-09-17T11:08:13Z\"\n    property=\"book:release_date\"\n  />,\n  <meta\n    content=\"Tag A\"\n    property=\"book:tag\"\n  />,\n  <meta\n    content=\"Tag B\"\n    property=\"book:tag\"\n  />,\n  <meta\n    content=\"https://www.test.ie/og-image-book-title-01.jpg\"\n    property=\"og:image\"\n  />,\n  <meta\n    content=\"Og Image Alt Book Title A\"\n    property=\"og:image:alt\"\n  />,\n  <meta\n    content=\"850\"\n    property=\"og:image:width\"\n  />,\n  <meta\n    content=\"650\"\n    property=\"og:image:height\"\n  />,\n  <meta\n    content=\"https://www.test.ie/og-image-book-title-02.jpg\"\n    property=\"og:image\"\n  />,\n  <meta\n    content=\"Og Image Alt Book Title B\"\n    property=\"og:image:alt\"\n  />,\n  <meta\n    content=\"950\"\n    property=\"og:image:width\"\n  />,\n  <meta\n    content=\"850\"\n    property=\"og:image:height\"\n  />,\n  <meta\n    content=\"SiteName\"\n    property=\"og:site_name\"\n  />,\n]\n`;\n\nexports[`buildTags and generateSeoTags > Profile SEO renders correctly 1`] = `\n[\n  <title>\n    Next SEO | Profile Page Title\n  </title>,\n  <meta\n    content=\"index,follow\"\n    name=\"robots\"\n  />,\n  <meta\n    content=\"Description of profile page\"\n    name=\"description\"\n  />,\n  <meta\n    content=\"summary_large_image\"\n    name=\"twitter:card\"\n  />,\n  <meta\n    content=\"@site\"\n    name=\"twitter:site\"\n  />,\n  <meta\n    content=\"@handle\"\n    name=\"twitter:creator\"\n  />,\n  <meta\n    content=\"Open Graph Profile Title\"\n    property=\"og:title\"\n  />,\n  <meta\n    content=\"Description of open graph profile\"\n    property=\"og:description\"\n  />,\n  <meta\n    content=\"https://www.example.com/@firstlast123\"\n    property=\"og:url\"\n  />,\n  <meta\n    content=\"profile\"\n    property=\"og:type\"\n  />,\n  <meta\n    content=\"First\"\n    property=\"profile:first_name\"\n  />,\n  <meta\n    content=\"Last\"\n    property=\"profile:last_name\"\n  />,\n  <meta\n    content=\"firstlast123\"\n    property=\"profile:username\"\n  />,\n  <meta\n    content=\"male\"\n    property=\"profile:gender\"\n  />,\n  <meta\n    content=\"https://www.test.ie/og-image-firstlast123-01.jpg\"\n    property=\"og:image\"\n  />,\n  <meta\n    content=\"Og Image Alt firstlast123 A\"\n    property=\"og:image:alt\"\n  />,\n  <meta\n    content=\"850\"\n    property=\"og:image:width\"\n  />,\n  <meta\n    content=\"650\"\n    property=\"og:image:height\"\n  />,\n  <meta\n    content=\"https://www.test.ie/og-image-firstlast123-02.jpg\"\n    property=\"og:image\"\n  />,\n  <meta\n    content=\"Og Image Alt firstlast123 B\"\n    property=\"og:image:alt\"\n  />,\n  <meta\n    content=\"950\"\n    property=\"og:image:width\"\n  />,\n  <meta\n    content=\"850\"\n    property=\"og:image:height\"\n  />,\n  <meta\n    content=\"SiteName\"\n    property=\"og:site_name\"\n  />,\n]\n`;\n\nexports[`buildTags and generateSeoTags > Video SEO renders correctly 1`] = `\n[\n  <title>\n    Next SEO | Video Page Title\n  </title>,\n  <meta\n    content=\"index,follow\"\n    name=\"robots\"\n  />,\n  <meta\n    content=\"Description of video page\"\n    name=\"description\"\n  />,\n  <meta\n    content=\"summary_large_image\"\n    name=\"twitter:card\"\n  />,\n  <meta\n    content=\"@site\"\n    name=\"twitter:site\"\n  />,\n  <meta\n    content=\"@handle\"\n    name=\"twitter:creator\"\n  />,\n  <meta\n    content=\"Open Graph Video Title\"\n    property=\"og:title\"\n  />,\n  <meta\n    content=\"Description of open graph video\"\n    property=\"og:description\"\n  />,\n  <meta\n    content=\"https://www.example.com/videos/video-title\"\n    property=\"og:url\"\n  />,\n  <meta\n    content=\"video.movie\"\n    property=\"og:type\"\n  />,\n  <meta\n    content=\"https://www.example.com/actors/@firstnameA-lastnameA\"\n    property=\"video:actor\"\n  />,\n  <meta\n    content=\"Protagonist\"\n    property=\"video:actor:role\"\n  />,\n  <meta\n    content=\"https://www.example.com/actors/@firstnameB-lastnameB\"\n    property=\"video:actor\"\n  />,\n  <meta\n    content=\"Antagonist\"\n    property=\"video:actor:role\"\n  />,\n  <meta\n    content=\"https://www.example.com/directors/@firstnameA-lastnameA\"\n    property=\"video:director\"\n  />,\n  <meta\n    content=\"https://www.example.com/directors/@firstnameB-lastnameB\"\n    property=\"video:director\"\n  />,\n  <meta\n    content=\"https://www.example.com/writers/@firstnameA-lastnameA\"\n    property=\"video:writer\"\n  />,\n  <meta\n    content=\"https://www.example.com/writers/@firstnameB-lastnameB\"\n    property=\"video:writer\"\n  />,\n  <meta\n    content=\"680000\"\n    property=\"video:duration\"\n  />,\n  <meta\n    content=\"2022-12-21T22:04:11Z\"\n    property=\"video:release_date\"\n  />,\n  <meta\n    content=\"Tag A\"\n    property=\"video:tag\"\n  />,\n  <meta\n    content=\"Tag B\"\n    property=\"video:tag\"\n  />,\n  <meta\n    content=\"https://www.test.ie/og-image-video-title-01.jpg\"\n    property=\"og:image\"\n  />,\n  <meta\n    content=\"Og Image Alt Video Title A\"\n    property=\"og:image:alt\"\n  />,\n  <meta\n    content=\"850\"\n    property=\"og:image:width\"\n  />,\n  <meta\n    content=\"650\"\n    property=\"og:image:height\"\n  />,\n  <meta\n    content=\"https://www.test.ie/og-image-video-title-02.jpg\"\n    property=\"og:image\"\n  />,\n  <meta\n    content=\"Og Image Alt Video Title B\"\n    property=\"og:image:alt\"\n  />,\n  <meta\n    content=\"950\"\n    property=\"og:image:width\"\n  />,\n  <meta\n    content=\"850\"\n    property=\"og:image:height\"\n  />,\n  <meta\n    content=\"SiteName\"\n    property=\"og:site_name\"\n  />,\n]\n`;\n\nexports[`buildTags and generateSeoTags > renders correctly 1`] = `\n[\n  <title>\n    This is a test title.\n  </title>,\n  <meta\n    content=\"index,follow\"\n    name=\"robots\"\n  />,\n  <meta\n    content=\"This is a test description.\"\n    name=\"description\"\n  />,\n  <meta\n    content=\"#73fa97\"\n    name=\"theme-color\"\n  />,\n  <link\n    href=\"https://m.canonical.ie\"\n    media=\"only screen and (max-width: 640px)\"\n    rel=\"alternate\"\n  />,\n  <link\n    href=\"https://www.canonical.ie/de\"\n    hrefLang=\"de-AT\"\n    rel=\"alternate\"\n  />,\n  <link\n    href=\"https://www.canonical.ie/sk\"\n    hrefLang=\"sk-SK\"\n    rel=\"alternate\"\n  />,\n  <meta\n    content=\"summary_large_image\"\n    name=\"twitter:card\"\n  />,\n  <meta\n    content=\"@site\"\n    name=\"twitter:site\"\n  />,\n  <meta\n    content=\"@handle\"\n    name=\"twitter:creator\"\n  />,\n  <meta\n    content=\"1234567890\"\n    property=\"fb:app_id\"\n  />,\n  <meta\n    content=\"Open graph title\"\n    property=\"og:title\"\n  />,\n  <meta\n    content=\"This is testing og:description.\"\n    property=\"og:description\"\n  />,\n  <meta\n    content=\"https://www.url.ie\"\n    property=\"og:url\"\n  />,\n  <meta\n    content=\"website\"\n    property=\"og:type\"\n  />,\n  <meta\n    content=\"https://www.test.ie/image-01.jpg\"\n    property=\"og:image\"\n  />,\n  <meta\n    content=\"Alt text right here\"\n    property=\"og:image:alt\"\n  />,\n  <meta\n    content=\"https://www.test.ie/secure-image-01.jpg\"\n    property=\"og:image:secure_url\"\n  />,\n  <meta\n    content=\"image/jpeg\"\n    property=\"og:image:type\"\n  />,\n  <meta\n    content=\"800\"\n    property=\"og:image:width\"\n  />,\n  <meta\n    content=\"600\"\n    property=\"og:image:height\"\n  />,\n  <meta\n    content=\"https://www.test.ie/image-02.jpg\"\n    property=\"og:image\"\n  />,\n  <meta\n    content=\"1200\"\n    property=\"og:image:width\"\n  />,\n  <meta\n    content=\"1200\"\n    property=\"og:image:height\"\n  />,\n  <meta\n    content=\"https://www.test.ie/image-03.jpg\"\n    property=\"og:image\"\n  />,\n  <meta\n    content=\"1200\"\n    property=\"og:image:width\"\n  />,\n  <meta\n    content=\"1200\"\n    property=\"og:image:height\"\n  />,\n  <meta\n    content=\"https://www.test.ie/image-04.jpg\"\n    property=\"og:image\"\n  />,\n  <meta\n    content=\"1200\"\n    property=\"og:image:width\"\n  />,\n  <meta\n    content=\"1200\"\n    property=\"og:image:height\"\n  />,\n  <meta\n    content=\"http://examples.opengraphprotocol.us/media/audio/1khz.mp3\"\n    property=\"og:audio\"\n  />,\n  <meta\n    content=\"https://d72cgtgi6hvvl.cloudfront.net/media/audio/1khz.mp3\"\n    property=\"og:audio:secure_url\"\n  />,\n  <meta\n    content=\"audio/mpeg\"\n    property=\"og:audio:type\"\n  />,\n  <meta\n    content=\"http://examples.opengraphprotocol.us/media/audio/250hz.mp3\"\n    property=\"og:audio\"\n  />,\n  <meta\n    content=\"https://d72cgtgi6hvvl.cloudfront.net/media/audio/250hz.mp3\"\n    property=\"og:audio:secure_url\"\n  />,\n  <meta\n    content=\"audio/mpeg\"\n    property=\"og:audio:type\"\n  />,\n  <meta\n    content=\"en_IE\"\n    property=\"og:locale\"\n  />,\n  <meta\n    content=\"SiteName\"\n    property=\"og:site_name\"\n  />,\n  <link\n    href=\"https://www.canonical.ie\"\n    rel=\"canonical\"\n  />,\n  <link\n    href=\"https://www.test.ie/favicon.ico\"\n    rel=\"icon\"\n  />,\n  <link\n    href=\"https://www.test.ie/touch-icon-ipad.jpg\"\n    rel=\"apple-touch-icon\"\n    sizes=\"76x76\"\n  />,\n  <link\n    href=\"https://www.test.ie/touch-icon-iphone-retina.jpg\"\n    rel=\"apple-touch-icon\"\n    sizes=\"120x120\"\n  />,\n  <link\n    color=\"#193860\"\n    href=\"https://www.test.ie/safari-pinned-tab.svg\"\n    rel=\"mask-icon\"\n  />,\n  <link\n    href=\"/manifest.json\"\n    rel=\"manifest\"\n  />,\n  <link\n    as=\"font\"\n    crossOrigin=\"anonymous\"\n    href=\"https://www.test.ie/font/sample-font.woof2\"\n    rel=\"preload\"\n    type=\"font/woff2\"\n  />,\n]\n`;\n"
  },
  {
    "path": "src/pages/core/buildTags.test.tsx",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { cleanup, render } from \"@testing-library/react\";\nimport { describe, it, expect, afterEach } from \"vitest\";\nimport { BuildTagsParams, ImagePrevSize } from \"../types\";\n\nimport {\n  generateSeoTags,\n  generateNextSeo,\n  generateDefaultSeo,\n} from \"./buildTags\";\n\ndescribe(\"buildTags and generateSeoTags\", () => {\n  afterEach(cleanup);\n\n  const SEO: BuildTagsParams = {\n    title: \"This is a test title.\",\n    themeColor: \"#73fa97\",\n    description: \"This is a test description.\",\n    canonical: \"https://www.canonical.ie\",\n    defaultOpenGraphImageHeight: 1200,\n    defaultOpenGraphImageWidth: 1200,\n    mobileAlternate: {\n      media: \"only screen and (max-width: 640px)\",\n      href: \"https://m.canonical.ie\",\n    },\n    languageAlternates: [\n      {\n        hrefLang: \"de-AT\",\n        href: \"https://www.canonical.ie/de\",\n      },\n      {\n        hrefLang: \"sk-SK\",\n        href: \"https://www.canonical.ie/sk\",\n      },\n    ],\n    additionalLinkTags: [\n      {\n        rel: \"icon\",\n        href: \"https://www.test.ie/favicon.ico\",\n      },\n      {\n        rel: \"apple-touch-icon\",\n        href: \"https://www.test.ie/touch-icon-ipad.jpg\",\n        sizes: \"76x76\",\n      },\n      {\n        rel: \"apple-touch-icon\",\n        href: \"https://www.test.ie/touch-icon-iphone-retina.jpg\",\n        sizes: \"120x120\",\n      },\n      {\n        rel: \"mask-icon\",\n        href: \"https://www.test.ie/safari-pinned-tab.svg\",\n        color: \"#193860\",\n      },\n      {\n        rel: \"manifest\",\n        href: \"/manifest.json\",\n      },\n      {\n        rel: \"preload\",\n        href: \"https://www.test.ie/font/sample-font.woof2\",\n        as: \"font\",\n        type: \"font/woff2\",\n        crossOrigin: \"anonymous\",\n      },\n    ],\n    openGraph: {\n      type: \"website\",\n      locale: \"en_IE\",\n      url: \"https://www.url.ie\",\n      title: \"Open graph title\",\n      description: \"This is testing og:description.\",\n      images: [\n        {\n          url: \"https://www.test.ie/image-01.jpg\",\n          width: 800,\n          height: 600,\n          alt: \"Alt text right here\",\n          type: \"image/jpeg\",\n          secureUrl: \"https://www.test.ie/secure-image-01.jpg\",\n        },\n        { url: \"https://www.test.ie/image-02.jpg\" },\n        { url: \"https://www.test.ie/image-03.jpg\" },\n        { url: \"https://www.test.ie/image-04.jpg\" },\n      ],\n      audio: [\n        {\n          url: \"http://examples.opengraphprotocol.us/media/audio/1khz.mp3\",\n          secureUrl:\n            \"https://d72cgtgi6hvvl.cloudfront.net/media/audio/1khz.mp3\",\n          type: \"audio/mpeg\",\n        },\n        {\n          url: \"http://examples.opengraphprotocol.us/media/audio/250hz.mp3\",\n          secureUrl:\n            \"https://d72cgtgi6hvvl.cloudfront.net/media/audio/250hz.mp3\",\n          type: \"audio/mpeg\",\n        },\n      ],\n      site_name: \"SiteName\",\n      siteName: \"SiteName\",\n    },\n    twitter: {\n      handle: \"@handle\",\n      site: \"@site\",\n      cardType: \"summary_large_image\",\n    },\n    facebook: {\n      appId: \"1234567890\",\n    },\n  };\n\n  it(\"renders correctly\", () => {\n    const tags = generateSeoTags(SEO);\n    expect(tags).toMatchSnapshot();\n  });\n\n  it(\"returns full array for default seo object\", () => {\n    const tags = generateSeoTags(SEO);\n    render(<>{tags}</>);\n\n    const title = Array.from(document.querySelectorAll(\"title\")).find(\n      (element) => element.textContent?.startsWith(`${SEO.title}`),\n    );\n    const index = document.querySelectorAll('meta[content=\"index,follow\"]');\n    const description = document.querySelectorAll(\n      `meta[content=\"${SEO.description}\"]`,\n    );\n    const descriptionTag = document.querySelectorAll(\n      'meta[name=\"description\"]',\n    );\n    const twitterCard = document.querySelectorAll(\n      'meta[content=\"summary_large_image\"]',\n    );\n    const facebookAppId = document.querySelectorAll(\n      'meta[property=\"fb:app_id\"]',\n    );\n    const twitterCardTag = document.querySelectorAll(\n      'meta[name=\"twitter:card\"]',\n    );\n    const twitterHandle = document.querySelectorAll(\n      `meta[content=\"${SEO.twitter?.handle}\"]`,\n    );\n    const twitterHandleTag = document.querySelectorAll(\n      'meta[name=\"twitter:creator\"]',\n    );\n    const twitterSite = document.querySelectorAll(\n      `meta[content=\"${SEO.twitter?.site}\"]`,\n    );\n    const twitterSiteTag = document.querySelectorAll(\n      'meta[name=\"twitter:site\"]',\n    );\n    const ogUrl = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.url}\"]`,\n    );\n    const ogUrlTag = document.querySelectorAll('meta[property=\"og:url\"]');\n    const ogType = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.type}\"]`,\n    );\n    const ogTypeTag = document.querySelectorAll('meta[property=\"og:type\"]');\n    const ogTitle = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.title}\"]`,\n    );\n    const ogTitleTag = document.querySelectorAll('meta[property=\"og:title\"]');\n    const ogDescription = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.description}\"]`,\n    );\n    const ogDescriptionTag = document.querySelectorAll(\n      'meta[property=\"og:description\"]',\n    );\n    const ogImage00 = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.images?.[0].url}\"]`,\n    );\n    const ogImageTag00 = tags.filter(\n      (item: any) => item?.key === \"og:image:01\",\n    );\n    const ogImage01 = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.images?.[1].url}\"]`,\n    );\n    const ogImageTag01 = tags.filter(\n      (item: any) => item?.key === \"og:image:01\",\n    );\n    const ogImage02 = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.images?.[2].url}\"]`,\n    );\n    const ogImageTag02 = tags.filter(\n      (item: any) => item?.key === \"og:image:02\",\n    );\n    const ogImage03 = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.images?.[3].url}\"]`,\n    );\n    const ogImageTag03 = tags.filter(\n      (item: any) => item?.key === \"og:image:03\",\n    );\n    const ogAudio00 = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.audio?.[0].url}\"]`,\n    );\n    const ogAudioTag00 = tags.filter(\n      (item: any) => item?.key === \"og:audio:01\",\n    );\n    const ogDefaultImageWidthHeight = document.querySelectorAll(\n      `meta[content=\"${SEO.defaultOpenGraphImageHeight}\"]`,\n    );\n    const ogSetImageHeight = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.images?.[0].height}\"]`,\n    );\n    const ogSetImageWidth = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.images?.[0].width}\"]`,\n    );\n    const ogSetImageAlt = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.images?.[0].alt}\"]`,\n    );\n    const ogSetImageType = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.images?.[0].type}\"]`,\n    );\n    const ogSetImageSecureUrl = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.images?.[0].secureUrl}\"]`,\n    );\n    const ogLocale = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.locale}\"]`,\n    );\n    const ogLocaleTag = tags.filter((item: any) => item?.key === \"og:locale\");\n    const ogSiteName = document.querySelectorAll(\n      `meta[content=\"${SEO.openGraph?.siteName || SEO.openGraph?.site_name}\"]`,\n    );\n    const ogSiteNameTag = tags.filter(\n      (item: any) => item?.key === \"og:site_name\",\n    );\n    const canonicalTag = tags.filter((item: any) => item?.key === \"canonical\");\n\n    const mobileAlternateTag = document.querySelectorAll(\n      'link[rel=\"alternate\"][media]',\n    );\n    const mobileAlternateHref = document.querySelectorAll(\n      `link[href=\"${SEO.mobileAlternate?.href}\"]`,\n    );\n    const mobileAlternateMedia = document.querySelectorAll(\n      `link[media=\"${SEO.mobileAlternate?.media}\"]`,\n    );\n\n    expect(Array.from(mobileAlternateTag).length).toBe(1);\n    expect(Array.from(mobileAlternateHref).length).toBe(1);\n    expect(Array.from(mobileAlternateMedia).length).toBe(1);\n\n    const languageAlternatesTags = document.querySelectorAll(\n      'link[rel=\"alternate\"][hrefLang]',\n    );\n    expect(Array.from(languageAlternatesTags).length).toBe(\n      SEO.languageAlternates?.length || 0,\n    );\n\n    SEO.languageAlternates?.forEach((_languageAlternate, idx) => {\n      const languageAlternateHref = document.querySelectorAll(\n        `link[href=\"${SEO.languageAlternates?.[idx].href}\"]`,\n      );\n      const languageAlternateHrefLang = document.querySelectorAll(\n        `link[hrefLang=\"${SEO.languageAlternates?.[idx].hrefLang}\"]`,\n      );\n\n      expect(Array.from(languageAlternateHref).length).toBe(1);\n      expect(Array.from(languageAlternateHrefLang).length).toBe(1);\n    });\n\n    expect(title).toBeDefined();\n    expect(Array.from(index).length).toBe(1);\n    expect(Array.from(description).length).toBe(1);\n    expect(Array.from(descriptionTag).length).toBe(1);\n    expect(Array.from(facebookAppId).length).toBe(1);\n    expect(Array.from(twitterCard).length).toBe(1);\n    expect(Array.from(twitterCardTag).length).toBe(1);\n    expect(Array.from(twitterHandle).length).toBe(1);\n    expect(Array.from(twitterHandleTag).length).toBe(1);\n    expect(Array.from(twitterSite).length).toBe(1);\n    expect(Array.from(twitterSiteTag).length).toBe(1);\n    expect(Array.from(ogUrl).length).toBe(1);\n    expect(Array.from(ogUrlTag).length).toBe(1);\n    expect(Array.from(ogType).length).toBe(1);\n    expect(Array.from(ogTypeTag).length).toBe(1);\n    expect(Array.from(ogTitle).length).toBe(1);\n    expect(Array.from(ogTitleTag).length).toBe(1);\n    expect(Array.from(ogDescription).length).toBe(1);\n    expect(Array.from(ogDescriptionTag).length).toBe(1);\n    expect(Array.from(ogImage00).length).toBe(1);\n    expect(Array.from(ogImageTag00).length).toBe(1);\n    expect(Array.from(ogImage01).length).toBe(1);\n    expect(Array.from(ogImageTag01).length).toBe(1);\n    expect(Array.from(ogImage02).length).toBe(1);\n    expect(Array.from(ogImageTag02).length).toBe(1);\n    expect(Array.from(ogImage03).length).toBe(1);\n    expect(Array.from(ogImageTag03).length).toBe(1);\n    expect(Array.from(ogAudio00).length).toBe(1);\n    expect(Array.from(ogAudioTag00).length).toBe(1);\n    expect(Array.from(ogDefaultImageWidthHeight).length).toBe(6);\n    expect(Array.from(ogSetImageHeight).length).toBe(1);\n    expect(Array.from(ogSetImageWidth).length).toBe(1);\n    expect(Array.from(ogSetImageAlt).length).toBe(1);\n    expect(Array.from(ogSetImageType).length).toBe(1);\n    expect(Array.from(ogSetImageSecureUrl).length).toBe(1);\n    expect(Array.from(ogLocale).length).toBe(1);\n    expect(Array.from(ogLocaleTag).length).toBe(1);\n    expect(Array.from(ogSiteName).length).toBe(1);\n    expect(Array.from(ogSiteNameTag).length).toBe(1);\n    expect((canonicalTag[0] as any)?.props?.href).toBe(`${SEO.canonical}`);\n    expect(Array.from(canonicalTag).length).toBe(1);\n  });\n\n  it(\"correctly sets noindex\", () => {\n    const overrideProps: BuildTagsParams = {\n      ...SEO,\n      noindex: true,\n    };\n    const tags = generateSeoTags(overrideProps);\n    render(<>{tags}</>);\n    const index = document.querySelectorAll('meta[content=\"index,follow\"]');\n    const noindex = document.querySelectorAll('meta[content=\"noindex,follow\"]');\n\n    expect(Array.from(index).length).toBe(0);\n    expect(Array.from(noindex).length).toBe(1);\n  });\n\n  it(\"correctly sets nofollow\", () => {\n    const overrideProps: BuildTagsParams = {\n      ...SEO,\n      nofollow: true,\n    };\n    const tags = generateSeoTags(overrideProps);\n    render(<>{tags}</>);\n    const indexfollow = document.querySelectorAll(\n      'meta[content=\"index,follow\"]',\n    );\n    const indexnofollow = document.querySelectorAll(\n      'meta[content=\"index,nofollow\"]',\n    );\n    expect(Array.from(indexfollow).length).toBe(0);\n    expect(Array.from(indexnofollow).length).toBe(1);\n  });\n\n  it(\"correctly sets noindex, nofollow\", () => {\n    const overrideProps: BuildTagsParams = {\n      ...SEO,\n      noindex: true,\n      nofollow: true,\n    };\n    const tags = generateSeoTags(overrideProps);\n    render(<>{tags}</>);\n    const indexfollow = document.querySelectorAll(\n      'meta[content=\"index,follow\"]',\n    );\n    const noindexnofollow = document.querySelectorAll(\n      'meta[content=\"noindex,nofollow\"]',\n    );\n\n    expect(Array.from(indexfollow).length).toBe(0);\n    expect(Array.from(noindexnofollow).length).toBe(1);\n  });\n\n  it(\"displays title with titleTemplate integrated\", () => {\n    const template = \"Next SEO\";\n    const overrideProps: BuildTagsParams = {\n      ...SEO,\n      titleTemplate: `${template} | %s`,\n    };\n    const tags = generateSeoTags(overrideProps);\n    render(<>{tags}</>);\n    const title = Array.from(document.querySelectorAll(\"title\")).find(\n      (element) => element.textContent?.startsWith(template),\n    );\n    expect(title?.innerHTML).toMatch(`${template} | ${SEO.title}`);\n  });\n\n  it(\"displays defaultTitle when no title is provided\", () => {\n    const defaultTitle = \"Next SEO\";\n    const props = {\n      titleTemplate: `${defaultTitle} | %s`,\n      defaultTitle,\n    };\n    const tags = generateSeoTags(props);\n    render(<>{tags}</>);\n    const title = Array.from(document.querySelectorAll(\"title\")).find(\n      (element) => element.textContent?.startsWith(defaultTitle),\n    );\n    const ogTitle = document.querySelectorAll(\n      `meta[content=\"${defaultTitle}\"]`,\n    );\n    const ogTitleTag = document.querySelectorAll('meta[property=\"og:title\"]');\n    expect(title?.innerHTML).toMatch(defaultTitle);\n    expect(Array.from(ogTitle).length).toBe(1);\n    expect(Array.from(ogTitleTag).length).toBe(1);\n  });\n\n  const ArticleSEO = {\n    title: \"Article Page Title\",\n    description: \"Description of article page\",\n    openGraph: {\n      title: \"Open Graph Article Title\",\n      description: \"Description of open graph article\",\n      url: \"https://www.example.com/articles/article-title\",\n      type: \"article\",\n      article: {\n        publishedTime: \"2017-06-21T23:04:13Z\",\n        modifiedTime: \"2018-01-21T18:04:43Z\",\n        expirationTime: \"2022-12-21T22:04:11Z\",\n        authors: [\n          \"https://www.example.com/authors/@firstnameA-lastnameA\",\n          \"https://www.example.com/authors/@firstnameB-lastnameB\",\n        ],\n        section: \"Section II\",\n        tags: [\"Tag A\", \"Tag B\"],\n      },\n      images: [\n        {\n          url: \"https://www.test.ie/og-image-article-title-01.jpg\",\n          width: 850,\n          height: 650,\n          alt: \"Og Image Alt Article Title A\",\n        },\n        {\n          url: \"https://www.test.ie/og-image-article-title-02.jpg\",\n          width: 950,\n          height: 850,\n          alt: \"Og Image Alt Article Title B\",\n        },\n      ],\n      siteName: \"SiteName\",\n      site_name: \"SiteName\",\n    },\n    twitter: {\n      handle: \"@handle\",\n      site: \"@site\",\n      cardType: \"summary_large_image\",\n    },\n  };\n\n  it(\"Article SEO renders correctly\", () => {\n    const tags = generateSeoTags(ArticleSEO);\n    expect(tags).toMatchSnapshot();\n  });\n\n  it(\"Check article og type meta\", () => {\n    const tags = generateSeoTags(ArticleSEO);\n    render(<>{tags}</>);\n\n    const ogType = document.querySelectorAll(\n      `meta[content=\"${ArticleSEO.openGraph.type}\"]`,\n    );\n    const ogTypeTag = document.querySelectorAll('meta[property=\"og:type\"]');\n    const ogArticlePublishedTime = document.querySelectorAll(\n      `meta[content=\"${ArticleSEO.openGraph.article.publishedTime}\"]`,\n    );\n    const ogArticlePublishedTimeTag = document.querySelectorAll(\n      'meta[property=\"article:published_time\"]',\n    );\n    const ogArticleModifiedTime = document.querySelectorAll(\n      `meta[content=\"${ArticleSEO.openGraph.article.modifiedTime}\"]`,\n    );\n    const ogArticleModifiedTimeTag = document.querySelectorAll(\n      'meta[property=\"article:modified_time\"]',\n    );\n    const ogArticleExpirationTime = document.querySelectorAll(\n      `meta[content=\"${ArticleSEO.openGraph.article.expirationTime}\"]`,\n    );\n    const ogArticleExpirationTimeTag = document.querySelectorAll(\n      'meta[property=\"article:expiration_time\"]',\n    );\n    const ogArticleAuthor00 = document.querySelectorAll(\n      `meta[content=\"${ArticleSEO.openGraph.article.authors[0]}\"]`,\n    );\n    const ogArticleAuthorTag00 = tags.filter(\n      (item: any) => item?.key === \"article:author:00\",\n    );\n    const ogArticleAuthor01 = document.querySelectorAll(\n      `meta[content=\"${ArticleSEO.openGraph.article.authors[1]}\"]`,\n    );\n    const ogArticleAuthorTag01 = tags.filter(\n      (item: any) => item?.key === \"article:author:01\",\n    );\n    const ogArticleSection = document.querySelectorAll(\n      `meta[content=\"${ArticleSEO.openGraph.article.section}\"]`,\n    );\n    const ogArticleSectionTag = document.querySelectorAll(\n      'meta[property=\"article:section\"]',\n    );\n    const ogArticleTags00 = document.querySelectorAll(\n      `meta[content=\"${ArticleSEO.openGraph.article.tags[0]}\"]`,\n    );\n    const ogArticleTagsTag00 = tags.filter(\n      (item: any) => item?.key === \"article:tag:00\",\n    );\n    const ogArticleTags01 = document.querySelectorAll(\n      `meta[content=\"${ArticleSEO.openGraph.article.tags[1]}\"]`,\n    );\n    const ogArticleTagsTag01 = tags.filter(\n      (item: any) => item?.key === \"article:tag:01\",\n    );\n\n    expect(Array.from(ogType).length).toBe(1);\n    expect(Array.from(ogTypeTag).length).toBe(1);\n    expect(Array.from(ogArticlePublishedTime).length).toBe(1);\n    expect(Array.from(ogArticlePublishedTimeTag).length).toBe(1);\n    expect(Array.from(ogArticleModifiedTime).length).toBe(1);\n    expect(Array.from(ogArticleModifiedTimeTag).length).toBe(1);\n    expect(Array.from(ogArticleExpirationTime).length).toBe(1);\n    expect(Array.from(ogArticleExpirationTimeTag).length).toBe(1);\n    expect(Array.from(ogArticleAuthor00).length).toBe(1);\n    expect(Array.from(ogArticleAuthorTag00).length).toBe(1);\n    expect(Array.from(ogArticleAuthor01).length).toBe(1);\n    expect(Array.from(ogArticleAuthorTag01).length).toBe(1);\n    expect(Array.from(ogArticleSection).length).toBe(1);\n    expect(Array.from(ogArticleSectionTag).length).toBe(1);\n    expect(Array.from(ogArticleTags00).length).toBe(1);\n    expect(Array.from(ogArticleTagsTag00).length).toBe(1);\n    expect(Array.from(ogArticleTags01).length).toBe(1);\n    expect(Array.from(ogArticleTagsTag01).length).toBe(1);\n  });\n\n  const BookSEO = {\n    title: \"Book Page Title\",\n    description: \"Description of book page\",\n    openGraph: {\n      title: \"Open Graph Book Title\",\n      description: \"Description of open graph book\",\n      url: \"https://www.example.com/books/book-title\",\n      type: \"book\",\n      book: {\n        releaseDate: \"2018-09-17T11:08:13Z\",\n        isbn: \"978-3-16-148410-0\",\n        authors: [\n          \"https://www.example.com/authors/@firstnameA-lastnameA\",\n          \"https://www.example.com/authors/@firstnameB-lastnameB\",\n        ],\n        tags: [\"Tag A\", \"Tag B\"],\n      },\n      images: [\n        {\n          url: \"https://www.test.ie/og-image-book-title-01.jpg\",\n          width: 850,\n          height: 650,\n          alt: \"Og Image Alt Book Title A\",\n        },\n        {\n          url: \"https://www.test.ie/og-image-book-title-02.jpg\",\n          width: 950,\n          height: 850,\n          alt: \"Og Image Alt Book Title B\",\n        },\n      ],\n      siteName: \"SiteName\",\n      site_name: \"SiteName\",\n    },\n    twitter: {\n      handle: \"@handle\",\n      site: \"@site\",\n      cardType: \"summary_large_image\",\n    },\n  };\n\n  it(\"Book SEO renders correctly\", () => {\n    const tags = generateSeoTags(BookSEO);\n    expect(tags).toMatchSnapshot();\n  });\n\n  it(\"Check book og type meta\", () => {\n    const tags = generateSeoTags(BookSEO);\n    render(<>{tags}</>);\n\n    const ogType = document.querySelectorAll(\n      `meta[content=\"${BookSEO.openGraph.type}\"]`,\n    );\n    const ogTypeTag = document.querySelectorAll('meta[property=\"og:type\"]');\n    const ogBookReleaseDate = document.querySelectorAll(\n      `meta[content=\"${BookSEO.openGraph.book.releaseDate}\"]`,\n    );\n    const ogBookReleaseDateTag = document.querySelectorAll(\n      'meta[property=\"book:release_date\"]',\n    );\n    const ogBookAuthor00 = document.querySelectorAll(\n      `meta[content=\"${BookSEO.openGraph.book.authors[0]}\"]`,\n    );\n    const ogBookAuthorTag00 = tags.filter(\n      (item: any) => item?.key === \"book:author:00\",\n    );\n    const ogBookAuthor01 = document.querySelectorAll(\n      `meta[content=\"${BookSEO.openGraph.book.authors[1]}\"]`,\n    );\n    const ogBookAuthorTag01 = tags.filter(\n      (item: any) => item?.key === \"book:author:01\",\n    );\n    const ogBookIsbn = document.querySelectorAll(\n      `meta[content=\"${BookSEO.openGraph.book.isbn}\"]`,\n    );\n    const ogBookIsbnTag = document.querySelectorAll(\n      'meta[property=\"book:isbn\"]',\n    );\n    const ogBookTags00 = document.querySelectorAll(\n      `meta[content=\"${BookSEO.openGraph.book.tags[0]}\"]`,\n    );\n    const ogBookTagsTag00 = tags.filter(\n      (item: any) => item?.key === \"book:tag:00\",\n    );\n    const ogBookTags01 = document.querySelectorAll(\n      `meta[content=\"${BookSEO.openGraph.book.tags[1]}\"]`,\n    );\n    const ogBookTagsTag01 = tags.filter(\n      (item: any) => item?.key === \"book:tag:01\",\n    );\n\n    expect(Array.from(ogType).length).toBe(1);\n    expect(Array.from(ogTypeTag).length).toBe(1);\n    expect(Array.from(ogBookReleaseDate).length).toBe(1);\n    expect(Array.from(ogBookReleaseDateTag).length).toBe(1);\n    expect(Array.from(ogBookAuthor00).length).toBe(1);\n    expect(Array.from(ogBookAuthorTag00).length).toBe(1);\n    expect(Array.from(ogBookAuthor01).length).toBe(1);\n    expect(Array.from(ogBookAuthorTag01).length).toBe(1);\n    expect(Array.from(ogBookIsbn).length).toBe(1);\n    expect(Array.from(ogBookIsbnTag).length).toBe(1);\n    expect(Array.from(ogBookTags00).length).toBe(1);\n    expect(Array.from(ogBookTagsTag00).length).toBe(1);\n    expect(Array.from(ogBookTags01).length).toBe(1);\n    expect(Array.from(ogBookTagsTag01).length).toBe(1);\n  });\n\n  const ProfileSEO = {\n    title: \"Profile Page Title\",\n    description: \"Description of profile page\",\n    openGraph: {\n      title: \"Open Graph Profile Title\",\n      description: \"Description of open graph profile\",\n      url: \"https://www.example.com/@firstlast123\",\n      type: \"profile\",\n      profile: {\n        firstName: \"First\",\n        lastName: \"Last\",\n        username: \"firstlast123\",\n        gender: \"male\",\n      },\n      images: [\n        {\n          url: \"https://www.test.ie/og-image-firstlast123-01.jpg\",\n          width: 850,\n          height: 650,\n          alt: \"Og Image Alt firstlast123 A\",\n        },\n        {\n          url: \"https://www.test.ie/og-image-firstlast123-02.jpg\",\n          width: 950,\n          height: 850,\n          alt: \"Og Image Alt firstlast123 B\",\n        },\n      ],\n      siteName: \"SiteName\",\n      site_name: \"SiteName\",\n    },\n    twitter: {\n      handle: \"@handle\",\n      site: \"@site\",\n      cardType: \"summary_large_image\",\n    },\n  };\n\n  it(\"Profile SEO renders correctly\", () => {\n    const tags = generateSeoTags(ProfileSEO);\n    expect(tags).toMatchSnapshot();\n  });\n\n  it(\"Check profile og type meta\", () => {\n    const tags = generateSeoTags(ProfileSEO);\n    render(<>{tags}</>);\n\n    const ogType = document.querySelectorAll(\n      `meta[content=\"${ProfileSEO.openGraph.type}\"]`,\n    );\n    const ogTypeTag = document.querySelectorAll('meta[property=\"og:type\"]');\n    const ogProfileFirstName = document.querySelectorAll(\n      `meta[content=\"${ProfileSEO.openGraph.profile.firstName}\"]`,\n    );\n    const ogProfileFirstNameTag = document.querySelectorAll(\n      'meta[property=\"profile:first_name\"]',\n    );\n    const ogProfileLastName = document.querySelectorAll(\n      `meta[content=\"${ProfileSEO.openGraph.profile.lastName}\"]`,\n    );\n    const ogProfileLastNameTag = document.querySelectorAll(\n      'meta[property=\"profile:last_name\"]',\n    );\n    const ogProfileUsername = document.querySelectorAll(\n      `meta[content=\"${ProfileSEO.openGraph.profile.username}\"]`,\n    );\n    const ogProfileUsernameTag = document.querySelectorAll(\n      'meta[property=\"profile:username\"]',\n    );\n    const ogProfileGender = document.querySelectorAll(\n      `meta[content=\"${ProfileSEO.openGraph.profile.gender}\"]`,\n    );\n    const ogProfileGenderTag = document.querySelectorAll(\n      'meta[property=\"profile:gender\"]',\n    );\n\n    expect(Array.from(ogType).length).toBe(1);\n    expect(Array.from(ogTypeTag).length).toBe(1);\n    expect(Array.from(ogProfileFirstName).length).toBe(1);\n    expect(Array.from(ogProfileFirstNameTag).length).toBe(1);\n    expect(Array.from(ogProfileLastName).length).toBe(1);\n    expect(Array.from(ogProfileLastNameTag).length).toBe(1);\n    expect(Array.from(ogProfileUsername).length).toBe(1);\n    expect(Array.from(ogProfileUsernameTag).length).toBe(1);\n    expect(Array.from(ogProfileGender).length).toBe(1);\n    expect(Array.from(ogProfileGenderTag).length).toBe(1);\n  });\n\n  const VideoSEO = {\n    title: \"Video Page Title\",\n    description: \"Description of video page\",\n    openGraph: {\n      title: \"Open Graph Video Title\",\n      description: \"Description of open graph video\",\n      url: \"https://www.example.com/videos/video-title\",\n      type: \"video.movie\",\n      video: {\n        actors: [\n          {\n            profile: \"https://www.example.com/actors/@firstnameA-lastnameA\",\n            role: \"Protagonist\",\n          },\n          {\n            profile: \"https://www.example.com/actors/@firstnameB-lastnameB\",\n            role: \"Antagonist\",\n          },\n        ],\n        directors: [\n          \"https://www.example.com/directors/@firstnameA-lastnameA\",\n          \"https://www.example.com/directors/@firstnameB-lastnameB\",\n        ],\n        writers: [\n          \"https://www.example.com/writers/@firstnameA-lastnameA\",\n          \"https://www.example.com/writers/@firstnameB-lastnameB\",\n        ],\n        duration: 680000,\n        releaseDate: \"2022-12-21T22:04:11Z\",\n        tags: [\"Tag A\", \"Tag B\"],\n      },\n      images: [\n        {\n          url: \"https://www.test.ie/og-image-video-title-01.jpg\",\n          width: 850,\n          height: 650,\n          alt: \"Og Image Alt Video Title A\",\n        },\n        {\n          url: \"https://www.test.ie/og-image-video-title-02.jpg\",\n          width: 950,\n          height: 850,\n          alt: \"Og Image Alt Video Title B\",\n        },\n      ],\n      siteName: \"SiteName\",\n      site_name: \"SiteName\",\n    },\n    twitter: {\n      handle: \"@handle\",\n      site: \"@site\",\n      cardType: \"summary_large_image\",\n    },\n  };\n\n  it(\"Video SEO renders correctly\", () => {\n    const tags = generateSeoTags(VideoSEO);\n    expect(tags).toMatchSnapshot();\n  });\n\n  it(\"Check video og type meta\", () => {\n    const tags = generateSeoTags(VideoSEO);\n    render(<>{tags}</>);\n\n    const ogType = document.querySelectorAll(\n      `meta[content=\"${VideoSEO.openGraph.type}\"]`,\n    );\n    const ogTypeTag = document.querySelectorAll('meta[property=\"og:type\"]');\n    const ogVideoReleaseDate = document.querySelectorAll(\n      `meta[content=\"${VideoSEO.openGraph.video.releaseDate}\"]`,\n    );\n    const ogVideoReleaseDateTag = document.querySelectorAll(\n      'meta[property=\"video:release_date\"]',\n    );\n    const ogVideoDuration = document.querySelectorAll(\n      `meta[content=\"${VideoSEO.openGraph.video.duration}\"]`,\n    );\n    const ogVideoDurationTag = document.querySelectorAll(\n      'meta[property=\"video:duration\"]',\n    );\n    const ogVideoActors00 = document.querySelectorAll(\n      `meta[content=\"${VideoSEO.openGraph.video.actors[0].profile}\"]`,\n    );\n    const ogVideoActorsTag00 = tags.filter(\n      (item: any) => item?.key === \"video:actor:00\",\n    );\n    const ogVideoActors01 = document.querySelectorAll(\n      `meta[content=\"${VideoSEO.openGraph.video.actors[1].profile}\"]`,\n    );\n    const ogVideoActorsTag01 = tags.filter(\n      (item: any) => item?.key === \"video:actor:01\",\n    );\n    const ogVideoActorsRoles00 = document.querySelectorAll(\n      `meta[content=\"${VideoSEO.openGraph.video.actors[0].role}\"]`,\n    );\n    const ogVideoActorsRolesTag00 = tags.filter(\n      (item: any) => item?.key === \"video:actor:role:00\",\n    );\n    const ogVideoActorsRoles01 = document.querySelectorAll(\n      `meta[content=\"${VideoSEO.openGraph.video.actors[1].role}\"]`,\n    );\n    const ogVideoActorsRolesTag01 = tags.filter(\n      (item: any) => item?.key === \"video:actor:role:01\",\n    );\n    const ogVideoDirectors00 = document.querySelectorAll(\n      `meta[content=\"${VideoSEO.openGraph.video.directors[0]}\"]`,\n    );\n    const ogVideoDirectorsTag00 = tags.filter(\n      (item: any) => item?.key === \"video:director:00\",\n    );\n    const ogVideoDirectors01 = document.querySelectorAll(\n      `meta[content=\"${VideoSEO.openGraph.video.directors[1]}\"]`,\n    );\n    const ogVideoDirectorsTag01 = tags.filter(\n      (item: any) => item?.key === \"video:director:01\",\n    );\n    const ogVideoWriters00 = document.querySelectorAll(\n      `meta[content=\"${VideoSEO.openGraph.video.writers[0]}\"]`,\n    );\n    const ogVideoWritersTag00 = tags.filter(\n      (item: any) => item?.key === \"video:writer:00\",\n    );\n    const ogVideoWriters01 = document.querySelectorAll(\n      `meta[content=\"${VideoSEO.openGraph.video.writers[1]}\"]`,\n    );\n    const ogVideoWritersTag01 = tags.filter(\n      (item: any) => item?.key === \"video:writer:01\",\n    );\n    const ogVideoTags00 = document.querySelectorAll(\n      `meta[content=\"${ArticleSEO.openGraph.article.tags[0]}\"]`,\n    );\n    const ogVideoTagsTag00 = tags.filter(\n      (item: any) => item?.key === \"video:tag:00\",\n    );\n    const ogVideoTags01 = document.querySelectorAll(\n      `meta[content=\"${VideoSEO.openGraph.video.tags[1]}\"]`,\n    );\n    const ogVideoTagsTag01 = tags.filter(\n      (item: any) => item?.key === \"video:tag:01\",\n    );\n\n    expect(Array.from(ogType).length).toBe(1);\n    expect(Array.from(ogTypeTag).length).toBe(1);\n    expect(Array.from(ogVideoReleaseDate).length).toBe(1);\n    expect(Array.from(ogVideoReleaseDateTag).length).toBe(1);\n    expect(Array.from(ogVideoDuration).length).toBe(1);\n    expect(Array.from(ogVideoDurationTag).length).toBe(1);\n    expect(Array.from(ogVideoActors00).length).toBe(1);\n    expect(Array.from(ogVideoActorsTag00).length).toBe(1);\n    expect(Array.from(ogVideoActors01).length).toBe(1);\n    expect(Array.from(ogVideoActorsTag01).length).toBe(1);\n    expect(Array.from(ogVideoActorsRoles00).length).toBe(1);\n    expect(Array.from(ogVideoActorsRolesTag00).length).toBe(1);\n    expect(Array.from(ogVideoActorsRoles01).length).toBe(1);\n    expect(Array.from(ogVideoActorsRolesTag01).length).toBe(1);\n    expect(Array.from(ogVideoDirectors00).length).toBe(1);\n    expect(Array.from(ogVideoDirectorsTag00).length).toBe(1);\n    expect(Array.from(ogVideoDirectors01).length).toBe(1);\n    expect(Array.from(ogVideoDirectorsTag01).length).toBe(1);\n    expect(Array.from(ogVideoWriters00).length).toBe(1);\n    expect(Array.from(ogVideoWritersTag00).length).toBe(1);\n    expect(Array.from(ogVideoWriters01).length).toBe(1);\n    expect(Array.from(ogVideoWritersTag01).length).toBe(1);\n    expect(Array.from(ogVideoTags00).length).toBe(1);\n    expect(Array.from(ogVideoTagsTag00).length).toBe(1);\n    expect(Array.from(ogVideoTags01).length).toBe(1);\n    expect(Array.from(ogVideoTagsTag01).length).toBe(1);\n  });\n\n  it(\"additional meta tags are set\", () => {\n    const overrideProps: BuildTagsParams = {\n      ...SEO,\n      additionalMetaTags: [\n        { property: \"random\", content: \"something\" },\n        { name: \"foo\", content: \"bar\" },\n        { httpEquiv: \"x-ua-compatible\", content: \"IE=edge; chrome=1\" },\n      ],\n    };\n    const tags = generateSeoTags(overrideProps);\n    render(<>{tags}</>);\n    const propertyTag = document.querySelectorAll('meta[content=\"something\"]');\n    const nameTag = document.querySelectorAll('meta[content=\"bar\"]');\n    const httpEquivTag = document.querySelectorAll(\n      'meta[content=\"IE=edge; chrome=1\"]',\n    );\n    expect(Array.from(propertyTag).length).toBe(1);\n    expect(Array.from(nameTag).length).toBe(1);\n    expect(Array.from(httpEquivTag).length).toBe(1);\n  });\n\n  it(\"uses key override to render multiple additional meta tags with the same key\", () => {\n    const overrideProps: BuildTagsParams = {\n      ...SEO,\n      additionalMetaTags: [\n        { property: \"foo\", content: \"Foo 1\", keyOverride: \"foo1\" },\n        { property: \"foo\", content: \"Foo 2\", keyOverride: \"foo2\" },\n        { name: \"bar\", content: \"Bar 1\", keyOverride: \"bar1\" },\n        { name: \"bar\", content: \"Bar 2\", keyOverride: \"bar2\" },\n      ],\n    };\n    const tags = generateSeoTags(overrideProps);\n    render(<>{tags}</>);\n\n    const propertyTags = document.querySelectorAll('meta[property=\"foo\"]');\n    expect(Array.from(propertyTags).length).toBe(2);\n    expect(propertyTags[0]).not.toHaveAttribute(\"keyoverride\");\n\n    const nameTags = document.querySelectorAll('meta[name=\"bar\"]');\n    expect(Array.from(nameTags).length).toBe(2);\n    expect(nameTags[0]).not.toHaveAttribute(\"keyoverride\");\n  });\n\n  it(\"correctly sets noindex default\", () => {\n    const overrideProps: BuildTagsParams = {\n      ...SEO,\n      dangerouslySetAllPagesToNoIndex: true,\n    };\n    const tags = generateSeoTags(overrideProps);\n    render(<>{tags}</>);\n    const indexfollow = document.querySelectorAll(\n      'meta[content=\"index,follow\"]',\n    );\n    const noindexfollow = document.querySelectorAll(\n      'meta[content=\"noindex,follow\"]',\n    );\n\n    expect(Array.from(indexfollow).length).toBe(0);\n    expect(Array.from(noindexfollow).length).toBe(1);\n  });\n\n  it(\"correctly sets nofollow default\", () => {\n    const overrideProps: BuildTagsParams = {\n      ...SEO,\n      dangerouslySetAllPagesToNoFollow: true,\n    };\n    const tags = generateSeoTags(overrideProps);\n    render(<>{tags}</>);\n    const indexfollow = document.querySelectorAll(\n      'meta[content=\"index,follow\"]',\n    );\n    const noindexnofollow = document.querySelectorAll(\n      'meta[content=\"noindex,nofollow\"]',\n    );\n\n    expect(Array.from(indexfollow).length).toBe(0);\n    expect(Array.from(noindexnofollow).length).toBe(1);\n  });\n\n  it(\"correctly read noindex & nofollow false\", () => {\n    const overrideProps: BuildTagsParams = {\n      ...SEO,\n      noindex: false,\n      nofollow: false,\n    };\n    const tags = generateSeoTags(overrideProps);\n    render(<>{tags}</>);\n    const indexfollow = document.querySelectorAll(\n      'meta[content=\"index,follow\"]',\n    );\n    const noindexnofollow = document.querySelectorAll(\n      'meta[content=\"noindex,nofollow\"]',\n    );\n\n    expect(Array.from(indexfollow).length).toBe(1);\n    expect(Array.from(noindexnofollow).length).toBe(0);\n  });\n\n  it(\"correctly read all robots props\", () => {\n    const overrideProps = {\n      ...SEO,\n      noindex: true,\n      nofollow: true,\n      robotsProps: {\n        nosnippet: true,\n        notranslate: true,\n        noimageindex: true,\n        noarchive: true,\n        maxSnippet: -1,\n        maxImagePreview: \"none\" as ImagePrevSize,\n        maxVideoPreview: -1,\n      },\n    };\n    const tags = generateSeoTags(overrideProps);\n    render(<>{tags}</>);\n    const content = document.querySelectorAll(\n      'meta[content=\"index,follow,nosnippet,max-snippet:-1,max-image-preview:none,noarchive,noimageindex,max-video-preview:-1,notranslate\"]',\n    );\n\n    const contentOverride = document.querySelectorAll(\n      'meta[content=\"noindex,nofollow,nosnippet,max-snippet:-1,max-image-preview:none,noarchive,noimageindex,max-video-preview:-1,notranslate\"]',\n    );\n    expect(Array.from(content).length).toBe(0);\n    expect(Array.from(contentOverride).length).toBe(1);\n  });\n\n  describe(\"new generate functions\", () => {\n    afterEach(cleanup); // Clean up after each test\n\n    it(\"generateNextSeo works correctly with titleTemplate override\", () => {\n      // Override any existing titleTemplate by explicitly setting it\n      render(\n        <>\n          {generateNextSeo({\n            title: \"Test NextSeo Title\",\n            description: \"Test NextSeo Description\",\n            titleTemplate: \"%s\", // Use %s to just show the title without template\n          })}\n        </>,\n      );\n\n      const title = document.querySelector(\"title\");\n      const description = document.querySelector('meta[name=\"description\"]');\n\n      expect(title?.textContent).toBe(\"Test NextSeo Title\");\n      expect(description?.getAttribute(\"content\")).toBe(\n        \"Test NextSeo Description\",\n      );\n    });\n\n    it(\"generateDefaultSeo works correctly\", () => {\n      render(\n        <>\n          {generateDefaultSeo({\n            defaultTitle: \"Default Site Title\",\n            description: \"Default Site Description\",\n            titleTemplate: \"MySite | %s\",\n          })}\n        </>,\n      );\n\n      const title = document.querySelector(\"title\");\n      const description = document.querySelector('meta[name=\"description\"]');\n\n      expect(title?.textContent).toBe(\"Default Site Title\");\n      expect(description?.getAttribute(\"content\")).toBe(\n        \"Default Site Description\",\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "src/pages/core/buildTags.tsx",
    "content": "import { ReactNode } from \"react\";\nimport {\n  BuildTagsParams,\n  OpenGraphMedia,\n  NextSeoProps,\n  DefaultSeoProps,\n} from \"../types\";\nconst defaults = {\n  templateTitle: \"\",\n  noindex: false,\n  nofollow: false,\n  norobots: false,\n  defaultOpenGraphImageWidth: 0,\n  defaultOpenGraphImageHeight: 0,\n  defaultOpenGraphVideoWidth: 0,\n  defaultOpenGraphVideoHeight: 0,\n};\n\nconst buildOpenGraphMediaTags = (\n  mediaType: \"image\" | \"video\" | \"audio\",\n  media: ReadonlyArray<OpenGraphMedia> = [],\n  {\n    defaultWidth,\n    defaultHeight,\n  }: { defaultWidth?: number; defaultHeight?: number } = {},\n) => {\n  return media.reduce((tags, medium, index) => {\n    tags.push(\n      <meta\n        key={`og:${mediaType}:0${index}`}\n        property={`og:${mediaType}`}\n        content={medium.url}\n      />,\n    );\n\n    if (medium.alt) {\n      tags.push(\n        <meta\n          key={`og:${mediaType}:alt0${index}`}\n          property={`og:${mediaType}:alt`}\n          content={medium.alt}\n        />,\n      );\n    }\n\n    if (medium.secureUrl) {\n      tags.push(\n        <meta\n          key={`og:${mediaType}:secure_url0${index}`}\n          property={`og:${mediaType}:secure_url`}\n          content={medium.secureUrl.toString()}\n        />,\n      );\n    }\n\n    if (medium.type) {\n      tags.push(\n        <meta\n          key={`og:${mediaType}:type0${index}`}\n          property={`og:${mediaType}:type`}\n          content={medium.type.toString()}\n        />,\n      );\n    }\n\n    if (medium.width) {\n      tags.push(\n        <meta\n          key={`og:${mediaType}:width0${index}`}\n          property={`og:${mediaType}:width`}\n          content={medium.width.toString()}\n        />,\n      );\n    } else if (defaultWidth) {\n      tags.push(\n        <meta\n          key={`og:${mediaType}:width0${index}`}\n          property={`og:${mediaType}:width`}\n          content={defaultWidth.toString()}\n        />,\n      );\n    }\n\n    if (medium.height) {\n      tags.push(\n        <meta\n          key={`og:${mediaType}:height${index}`}\n          property={`og:${mediaType}:height`}\n          content={medium.height.toString()}\n        />,\n      );\n    } else if (defaultHeight) {\n      tags.push(\n        <meta\n          key={`og:${mediaType}:height${index}`}\n          property={`og:${mediaType}:height`}\n          content={defaultHeight.toString()}\n        />,\n      );\n    }\n\n    return tags;\n  }, [] as ReactNode[]);\n};\n\nconst generateSeoTags = (config: BuildTagsParams) => {\n  const tagsToRender: ReactNode[] = [];\n\n  if (config.titleTemplate) {\n    defaults.templateTitle = config.titleTemplate;\n  }\n\n  let updatedTitle = \"\";\n  if (config.title) {\n    updatedTitle = config.title;\n    if (defaults.templateTitle) {\n      updatedTitle = defaults.templateTitle.replace(/%s/g, () => updatedTitle);\n    }\n  } else if (config.defaultTitle) {\n    updatedTitle = config.defaultTitle;\n  }\n\n  if (updatedTitle) {\n    tagsToRender.push(<title key=\"title\">{updatedTitle}</title>);\n  }\n\n  const noindex =\n    config.noindex === undefined\n      ? defaults.noindex || config.dangerouslySetAllPagesToNoIndex\n      : config.noindex;\n\n  const nofollow =\n    config.nofollow === undefined\n      ? defaults.nofollow || config.dangerouslySetAllPagesToNoFollow\n      : config.nofollow;\n\n  const norobots = config.norobots || defaults.norobots;\n\n  let robotsParams = \"\";\n\n  if (config.robotsProps) {\n    const {\n      nosnippet,\n      maxSnippet,\n      maxImagePreview,\n      maxVideoPreview,\n      noarchive,\n      noimageindex,\n      notranslate,\n      unavailableAfter,\n    } = config.robotsProps;\n\n    robotsParams = `${nosnippet ? \",nosnippet\" : \"\"}${\n      maxSnippet ? `,max-snippet:${maxSnippet}` : \"\"\n    }${maxImagePreview ? `,max-image-preview:${maxImagePreview}` : \"\"}${\n      noarchive ? \",noarchive\" : \"\"\n    }${unavailableAfter ? `,unavailable_after:${unavailableAfter}` : \"\"}${\n      noimageindex ? \",noimageindex\" : \"\"\n    }${maxVideoPreview ? `,max-video-preview:${maxVideoPreview}` : \"\"}${\n      notranslate ? \",notranslate\" : \"\"\n    }`;\n  }\n\n  if (config.norobots) {\n    defaults.norobots = true;\n  }\n\n  if (noindex || nofollow) {\n    if (config.dangerouslySetAllPagesToNoIndex) {\n      defaults.noindex = true;\n    }\n    if (config.dangerouslySetAllPagesToNoFollow) {\n      defaults.nofollow = true;\n    }\n\n    tagsToRender.push(\n      <meta\n        key=\"robots\"\n        name=\"robots\"\n        content={`${noindex ? \"noindex\" : \"index\"},${\n          nofollow ? \"nofollow\" : \"follow\"\n        }${robotsParams}`}\n      />,\n    );\n  } else if (!norobots || robotsParams) {\n    tagsToRender.push(\n      <meta\n        key=\"robots\"\n        name=\"robots\"\n        content={`index,follow${robotsParams}`}\n      />,\n    );\n  }\n\n  if (config.description) {\n    tagsToRender.push(\n      <meta\n        key=\"description\"\n        name=\"description\"\n        content={config.description}\n      />,\n    );\n  }\n\n  if (config.themeColor) {\n    tagsToRender.push(\n      <meta key=\"theme-color\" name=\"theme-color\" content={config.themeColor} />,\n    );\n  }\n\n  if (config.mobileAlternate) {\n    tagsToRender.push(\n      <link\n        rel=\"alternate\"\n        key=\"mobileAlternate\"\n        media={config.mobileAlternate.media}\n        href={config.mobileAlternate.href}\n      />,\n    );\n  }\n\n  if (config.languageAlternates && config.languageAlternates.length > 0) {\n    config.languageAlternates.forEach((languageAlternate) => {\n      tagsToRender.push(\n        <link\n          rel=\"alternate\"\n          key={`languageAlternate-${languageAlternate.hrefLang}`}\n          hrefLang={languageAlternate.hrefLang}\n          href={languageAlternate.href}\n        />,\n      );\n    });\n  }\n\n  if (config.twitter) {\n    if (config.twitter.cardType) {\n      tagsToRender.push(\n        <meta\n          key=\"twitter:card\"\n          name=\"twitter:card\"\n          content={config.twitter.cardType}\n        />,\n      );\n    }\n\n    if (config.twitter.site) {\n      tagsToRender.push(\n        <meta\n          key=\"twitter:site\"\n          name=\"twitter:site\"\n          content={config.twitter.site}\n        />,\n      );\n    }\n\n    if (config.twitter.handle) {\n      tagsToRender.push(\n        <meta\n          key=\"twitter:creator\"\n          name=\"twitter:creator\"\n          content={config.twitter.handle}\n        />,\n      );\n    }\n  }\n\n  if (config.facebook) {\n    if (config.facebook.appId) {\n      tagsToRender.push(\n        <meta\n          key=\"fb:app_id\"\n          property=\"fb:app_id\"\n          content={config.facebook.appId}\n        />,\n      );\n    }\n  }\n\n  if (config.openGraph?.title || updatedTitle) {\n    tagsToRender.push(\n      <meta\n        key=\"og:title\"\n        property=\"og:title\"\n        content={config.openGraph?.title || updatedTitle}\n      />,\n    );\n  }\n\n  if (config.openGraph?.description || config.description) {\n    tagsToRender.push(\n      <meta\n        key=\"og:description\"\n        property=\"og:description\"\n        content={config.openGraph?.description || config.description}\n      />,\n    );\n  }\n\n  if (config.openGraph) {\n    if (config.openGraph.url || config.canonical) {\n      tagsToRender.push(\n        <meta\n          key=\"og:url\"\n          property=\"og:url\"\n          content={config.openGraph.url || config.canonical}\n        />,\n      );\n    }\n\n    if (config.openGraph.type) {\n      const type = config.openGraph.type.toLowerCase();\n\n      tagsToRender.push(\n        <meta key=\"og:type\" property=\"og:type\" content={type} />,\n      );\n\n      if (type === \"profile\" && config.openGraph.profile) {\n        if (config.openGraph.profile.firstName) {\n          tagsToRender.push(\n            <meta\n              key=\"profile:first_name\"\n              property=\"profile:first_name\"\n              content={config.openGraph.profile.firstName}\n            />,\n          );\n        }\n\n        if (config.openGraph.profile.lastName) {\n          tagsToRender.push(\n            <meta\n              key=\"profile:last_name\"\n              property=\"profile:last_name\"\n              content={config.openGraph.profile.lastName}\n            />,\n          );\n        }\n\n        if (config.openGraph.profile.username) {\n          tagsToRender.push(\n            <meta\n              key=\"profile:username\"\n              property=\"profile:username\"\n              content={config.openGraph.profile.username}\n            />,\n          );\n        }\n\n        if (config.openGraph.profile.gender) {\n          tagsToRender.push(\n            <meta\n              key=\"profile:gender\"\n              property=\"profile:gender\"\n              content={config.openGraph.profile.gender}\n            />,\n          );\n        }\n      } else if (type === \"book\" && config.openGraph.book) {\n        if (\n          config.openGraph.book.authors &&\n          config.openGraph.book.authors.length\n        ) {\n          config.openGraph.book.authors.forEach((author, index) => {\n            tagsToRender.push(\n              <meta\n                key={`book:author:0${index}`}\n                property=\"book:author\"\n                content={author}\n              />,\n            );\n          });\n        }\n\n        if (config.openGraph.book.isbn) {\n          tagsToRender.push(\n            <meta\n              key=\"book:isbn\"\n              property=\"book:isbn\"\n              content={config.openGraph.book.isbn}\n            />,\n          );\n        }\n\n        if (config.openGraph.book.releaseDate) {\n          tagsToRender.push(\n            <meta\n              key=\"book:release_date\"\n              property=\"book:release_date\"\n              content={config.openGraph.book.releaseDate}\n            />,\n          );\n        }\n\n        if (config.openGraph.book.tags && config.openGraph.book.tags.length) {\n          config.openGraph.book.tags.forEach((tag, index) => {\n            tagsToRender.push(\n              <meta\n                key={`book:tag:0${index}`}\n                property=\"book:tag\"\n                content={tag}\n              />,\n            );\n          });\n        }\n      } else if (type === \"article\" && config.openGraph.article) {\n        if (config.openGraph.article.publishedTime) {\n          tagsToRender.push(\n            <meta\n              key=\"article:published_time\"\n              property=\"article:published_time\"\n              content={config.openGraph.article.publishedTime}\n            />,\n          );\n        }\n\n        if (config.openGraph.article.modifiedTime) {\n          tagsToRender.push(\n            <meta\n              key=\"article:modified_time\"\n              property=\"article:modified_time\"\n              content={config.openGraph.article.modifiedTime}\n            />,\n          );\n        }\n\n        if (config.openGraph.article.expirationTime) {\n          tagsToRender.push(\n            <meta\n              key=\"article:expiration_time\"\n              property=\"article:expiration_time\"\n              content={config.openGraph.article.expirationTime}\n            />,\n          );\n        }\n\n        if (\n          config.openGraph.article.authors &&\n          config.openGraph.article.authors.length\n        ) {\n          config.openGraph.article.authors.forEach((author, index) => {\n            tagsToRender.push(\n              <meta\n                key={`article:author:0${index}`}\n                property=\"article:author\"\n                content={author}\n              />,\n            );\n          });\n        }\n\n        if (config.openGraph.article.section) {\n          tagsToRender.push(\n            <meta\n              key=\"article:section\"\n              property=\"article:section\"\n              content={config.openGraph.article.section}\n            />,\n          );\n        }\n\n        if (\n          config.openGraph.article.tags &&\n          config.openGraph.article.tags.length\n        ) {\n          config.openGraph.article.tags.forEach((tag, index) => {\n            tagsToRender.push(\n              <meta\n                key={`article:tag:0${index}`}\n                property=\"article:tag\"\n                content={tag}\n              />,\n            );\n          });\n        }\n      } else if (\n        (type === \"video.movie\" ||\n          type === \"video.episode\" ||\n          type === \"video.tv_show\" ||\n          type === \"video.other\") &&\n        config.openGraph.video\n      ) {\n        if (\n          config.openGraph.video.actors &&\n          config.openGraph.video.actors.length\n        ) {\n          config.openGraph.video.actors.forEach((actor, index) => {\n            if (actor.profile) {\n              tagsToRender.push(\n                <meta\n                  key={`video:actor:0${index}`}\n                  property=\"video:actor\"\n                  content={actor.profile}\n                />,\n              );\n            }\n\n            if (actor.role) {\n              tagsToRender.push(\n                <meta\n                  key={`video:actor:role:0${index}`}\n                  property=\"video:actor:role\"\n                  content={actor.role}\n                />,\n              );\n            }\n          });\n        }\n\n        if (\n          config.openGraph.video.directors &&\n          config.openGraph.video.directors.length\n        ) {\n          config.openGraph.video.directors.forEach((director, index) => {\n            tagsToRender.push(\n              <meta\n                key={`video:director:0${index}`}\n                property=\"video:director\"\n                content={director}\n              />,\n            );\n          });\n        }\n\n        if (\n          config.openGraph.video.writers &&\n          config.openGraph.video.writers.length\n        ) {\n          config.openGraph.video.writers.forEach((writer, index) => {\n            tagsToRender.push(\n              <meta\n                key={`video:writer:0${index}`}\n                property=\"video:writer\"\n                content={writer}\n              />,\n            );\n          });\n        }\n\n        if (config.openGraph.video.duration) {\n          tagsToRender.push(\n            <meta\n              key=\"video:duration\"\n              property=\"video:duration\"\n              content={config.openGraph.video.duration.toString()}\n            />,\n          );\n        }\n\n        if (config.openGraph.video.releaseDate) {\n          tagsToRender.push(\n            <meta\n              key=\"video:release_date\"\n              property=\"video:release_date\"\n              content={config.openGraph.video.releaseDate}\n            />,\n          );\n        }\n\n        if (config.openGraph.video.tags && config.openGraph.video.tags.length) {\n          config.openGraph.video.tags.forEach((tag, index) => {\n            tagsToRender.push(\n              <meta\n                key={`video:tag:0${index}`}\n                property=\"video:tag\"\n                content={tag}\n              />,\n            );\n          });\n        }\n\n        if (config.openGraph.video.series) {\n          tagsToRender.push(\n            <meta\n              key=\"video:series\"\n              property=\"video:series\"\n              content={config.openGraph.video.series}\n            />,\n          );\n        }\n      }\n    }\n\n    // images\n    if (config.defaultOpenGraphImageWidth) {\n      defaults.defaultOpenGraphImageWidth = config.defaultOpenGraphImageWidth;\n    }\n\n    if (config.defaultOpenGraphImageHeight) {\n      defaults.defaultOpenGraphImageHeight = config.defaultOpenGraphImageHeight;\n    }\n\n    if (config.openGraph.images && config.openGraph.images.length) {\n      tagsToRender.push(\n        ...buildOpenGraphMediaTags(\"image\", config.openGraph.images, {\n          defaultWidth: defaults.defaultOpenGraphImageWidth,\n          defaultHeight: defaults.defaultOpenGraphImageHeight,\n        }),\n      );\n    }\n\n    // videos\n    if (config.defaultOpenGraphVideoWidth) {\n      defaults.defaultOpenGraphVideoWidth = config.defaultOpenGraphVideoWidth;\n    }\n\n    if (config.defaultOpenGraphVideoHeight) {\n      defaults.defaultOpenGraphVideoHeight = config.defaultOpenGraphVideoHeight;\n    }\n\n    if (config.openGraph.videos && config.openGraph.videos.length) {\n      tagsToRender.push(\n        ...buildOpenGraphMediaTags(\"video\", config.openGraph.videos, {\n          defaultWidth: defaults.defaultOpenGraphVideoWidth,\n          defaultHeight: defaults.defaultOpenGraphVideoHeight,\n        }),\n      );\n    }\n\n    // audio\n    if (config.openGraph.audio) {\n      tagsToRender.push(\n        ...buildOpenGraphMediaTags(\"audio\", config.openGraph.audio),\n      );\n    }\n\n    if (config.openGraph.locale) {\n      tagsToRender.push(\n        <meta\n          key=\"og:locale\"\n          property=\"og:locale\"\n          content={config.openGraph.locale}\n        />,\n      );\n    }\n\n    if (config.openGraph.siteName || config.openGraph.site_name) {\n      tagsToRender.push(\n        <meta\n          key=\"og:site_name\"\n          property=\"og:site_name\"\n          content={config.openGraph.siteName || config.openGraph.site_name}\n        />,\n      );\n    }\n  }\n\n  if (config.canonical) {\n    tagsToRender.push(\n      <link rel=\"canonical\" href={config.canonical} key=\"canonical\" />,\n    );\n  }\n\n  if (config.additionalMetaTags && config.additionalMetaTags.length > 0) {\n    config.additionalMetaTags.forEach(({ keyOverride, ...tag }) => {\n      tagsToRender.push(\n        <meta\n          key={`meta:${\n            keyOverride ?? tag.name ?? tag.property ?? tag.httpEquiv\n          }`}\n          {...tag}\n        />,\n      );\n    });\n  }\n\n  if (config.additionalLinkTags?.length) {\n    config.additionalLinkTags.forEach((tag) => {\n      const { crossOrigin: tagCrossOrigin, ...rest } = tag;\n      const crossOrigin: \"anonymous\" | \"use-credentials\" | \"\" | undefined =\n        tagCrossOrigin === \"anonymous\" ||\n        tagCrossOrigin === \"use-credentials\" ||\n        tagCrossOrigin === \"\"\n          ? tagCrossOrigin\n          : undefined;\n\n      tagsToRender.push(\n        <link\n          key={`link${rest.keyOverride ?? rest.href}${rest.rel}`}\n          {...rest}\n          crossOrigin={crossOrigin}\n        />,\n      );\n    });\n  }\n\n  return tagsToRender;\n};\n\n/**\n * Generate SEO meta tags for Next.js\n * This is the core function that creates all SEO-related tags\n * @internal\n */\nexport { generateSeoTags };\n\n/**\n * Generate SEO meta tags for NextSeo component\n * Use this when you want to add SEO tags directly in Next.js <Head>\n * without using the NextSeo component wrapper\n *\n * @example\n * ```tsx\n * import Head from 'next/head';\n * import { generateNextSeo } from 'next-seo/pages';\n *\n * export default function Page() {\n *   return (\n *     <>\n *       <Head>\n *         {generateNextSeo({\n *           title: \"My Page Title\",\n *           description: \"My page description\"\n *         })}\n *       </Head>\n *       <h1>Page Content</h1>\n *     </>\n *   );\n * }\n * ```\n */\nexport function generateNextSeo(props: NextSeoProps): ReactNode[] {\n  return generateSeoTags(props);\n}\n\n/**\n * Generate default SEO meta tags for DefaultSeo component\n * Use this when you want to set global SEO defaults directly in Next.js <Head>\n * without using the DefaultSeo component wrapper\n *\n * @example\n * ```tsx\n * // pages/_app.tsx\n * import Head from 'next/head';\n * import { generateDefaultSeo } from 'next-seo/pages';\n *\n * export default function MyApp({ Component, pageProps }) {\n *   return (\n *     <>\n *       <Head>\n *         {generateDefaultSeo({\n *           titleTemplate: \"MySite | %s\",\n *           defaultTitle: \"MySite\",\n *           description: \"Default site description\"\n *         })}\n *       </Head>\n *       <Component {...pageProps} />\n *     </>\n *   );\n * }\n * ```\n */\nexport function generateDefaultSeo(props: DefaultSeoProps): ReactNode[] {\n  const {\n    title,\n    titleTemplate,\n    defaultTitle,\n    themeColor,\n    dangerouslySetAllPagesToNoIndex = false,\n    dangerouslySetAllPagesToNoFollow = false,\n    description,\n    canonical,\n    facebook,\n    openGraph,\n    additionalMetaTags,\n    twitter,\n    defaultOpenGraphImageWidth,\n    defaultOpenGraphImageHeight,\n    defaultOpenGraphVideoWidth,\n    defaultOpenGraphVideoHeight,\n    mobileAlternate,\n    languageAlternates,\n    additionalLinkTags,\n    robotsProps,\n    norobots,\n  } = props;\n  return generateSeoTags({\n    title,\n    titleTemplate,\n    defaultTitle,\n    themeColor,\n    dangerouslySetAllPagesToNoIndex,\n    dangerouslySetAllPagesToNoFollow,\n    description,\n    canonical,\n    facebook,\n    openGraph,\n    additionalMetaTags,\n    twitter,\n    defaultOpenGraphImageWidth,\n    defaultOpenGraphImageHeight,\n    defaultOpenGraphVideoWidth,\n    defaultOpenGraphVideoHeight,\n    mobileAlternate,\n    languageAlternates,\n    additionalLinkTags,\n    robotsProps,\n    norobots,\n  });\n}\n"
  },
  {
    "path": "src/pages/index.ts",
    "content": "/**\n * Next SEO - Pages Router Components\n *\n * For use with Next.js Pages Router only.\n * Import from 'next-seo/pages'\n */\n\n// Export tag generation functions for direct use in Next.js <Head>\nexport { generateNextSeo, generateDefaultSeo } from \"./core/buildTags\";\n\n// Export all types for TypeScript users\nexport type {\n  NextSeoProps,\n  DefaultSeoProps,\n  OpenGraph,\n  OpenGraphMedia,\n  OpenGraphProfile,\n  OpenGraphBook,\n  OpenGraphArticle,\n  OpenGraphVideo,\n  OpenGraphVideoActors,\n  Twitter,\n  MobileAlternate,\n  LanguageAlternate,\n  LinkTag,\n  MetaTag,\n  BaseMetaTag,\n  HTML5MetaTag,\n  RDFaMetaTag,\n  HTTPEquivMetaTag,\n  AdditionalRobotsProps,\n  ImagePrevSize,\n} from \"./types\";\n"
  },
  {
    "path": "src/pages/types/index.ts",
    "content": "// Types for Pages Router SEO components (next-seo/pages)\n\nexport interface OpenGraphMedia {\n  url: string;\n  width?: number;\n  height?: number;\n  alt?: string;\n  type?: string;\n  secureUrl?: string;\n}\n\nexport interface OpenGraphVideoActors {\n  profile: string;\n  role?: string;\n}\n\nexport interface OpenGraph {\n  url?: string;\n  type?: string;\n  title?: string;\n  description?: string;\n  images?: ReadonlyArray<OpenGraphMedia>;\n  videos?: ReadonlyArray<OpenGraphMedia>;\n  audio?: ReadonlyArray<OpenGraphMedia>;\n  defaultImageHeight?: number;\n  defaultImageWidth?: number;\n  locale?: string;\n  siteName?: string;\n  site_name?: string; // Deprecated but kept for backward compatibility\n  profile?: OpenGraphProfile;\n  book?: OpenGraphBook;\n  article?: OpenGraphArticle;\n  video?: OpenGraphVideo;\n}\n\nexport interface OpenGraphProfile {\n  firstName?: string;\n  lastName?: string;\n  username?: string;\n  gender?: string;\n}\n\nexport interface OpenGraphBook {\n  authors?: ReadonlyArray<string>;\n  isbn?: string;\n  releaseDate?: string;\n  tags?: ReadonlyArray<string>;\n}\n\nexport interface OpenGraphArticle {\n  publishedTime?: string;\n  modifiedTime?: string;\n  expirationTime?: string;\n  authors?: ReadonlyArray<string>;\n  section?: string;\n  tags?: ReadonlyArray<string>;\n}\n\nexport interface OpenGraphVideo {\n  actors?: ReadonlyArray<OpenGraphVideoActors>;\n  directors?: ReadonlyArray<string>;\n  writers?: ReadonlyArray<string>;\n  duration?: number;\n  releaseDate?: string;\n  tags?: ReadonlyArray<string>;\n  series?: string;\n}\n\nexport interface Twitter {\n  handle?: string;\n  site?: string;\n  cardType?: string;\n}\n\nexport interface MobileAlternate {\n  media: string;\n  href: string;\n}\n\nexport interface LanguageAlternate {\n  hrefLang: string;\n  href: string;\n}\n\nexport interface LinkTag {\n  rel: string;\n  href: string;\n  hrefLang?: string;\n  media?: string;\n  sizes?: string;\n  type?: string;\n  as?: string;\n  crossOrigin?: string;\n  imagesrcset?: string;\n  imagesizes?: string;\n  referrerpolicy?: string;\n  integrity?: string;\n  keyOverride?: string;\n  color?: string;\n}\n\nexport interface BaseMetaTag {\n  content: string;\n  keyOverride?: string;\n}\n\nexport interface HTML5MetaTag extends BaseMetaTag {\n  name: string;\n  property?: undefined;\n  httpEquiv?: undefined;\n}\n\nexport interface RDFaMetaTag extends BaseMetaTag {\n  property: string;\n  name?: undefined;\n  httpEquiv?: undefined;\n}\n\nexport interface HTTPEquivMetaTag extends BaseMetaTag {\n  httpEquiv:\n    | \"content-security-policy\"\n    | \"content-type\"\n    | \"default-style\"\n    | \"x-ua-compatible\"\n    | \"refresh\";\n  name?: undefined;\n  property?: undefined;\n}\n\nexport type MetaTag = HTML5MetaTag | RDFaMetaTag | HTTPEquivMetaTag;\n\nexport type ImagePrevSize = \"none\" | \"standard\" | \"large\";\n\nexport interface AdditionalRobotsProps {\n  nosnippet?: boolean;\n  maxSnippet?: number;\n  maxImagePreview?: ImagePrevSize;\n  maxVideoPreview?: number;\n  noarchive?: boolean;\n  unavailableAfter?: string;\n  noimageindex?: boolean;\n  notranslate?: boolean;\n}\n\nexport interface NextSeoProps {\n  title?: string;\n  titleTemplate?: string;\n  defaultTitle?: string;\n  themeColor?: string;\n  noindex?: boolean;\n  nofollow?: boolean;\n  robotsProps?: AdditionalRobotsProps;\n  description?: string;\n  canonical?: string;\n  mobileAlternate?: MobileAlternate;\n  languageAlternates?: ReadonlyArray<LanguageAlternate>;\n  openGraph?: OpenGraph;\n  facebook?: { appId: string };\n  twitter?: Twitter;\n  additionalMetaTags?: ReadonlyArray<MetaTag>;\n  additionalLinkTags?: ReadonlyArray<LinkTag>;\n}\n\nexport interface DefaultSeoProps extends NextSeoProps {\n  dangerouslySetAllPagesToNoIndex?: boolean;\n  dangerouslySetAllPagesToNoFollow?: boolean;\n  defaultOpenGraphImageWidth?: number;\n  defaultOpenGraphImageHeight?: number;\n  defaultOpenGraphVideoWidth?: number;\n  defaultOpenGraphVideoHeight?: number;\n  norobots?: boolean;\n}\n\nexport interface BuildTagsParams extends DefaultSeoProps, NextSeoProps {}\n"
  },
  {
    "path": "src/pages/utils/processors.ts",
    "content": "import type { OpenGraphMedia } from \"../types\";\n\n/**\n * Process OpenGraph media items (images, videos, audio)\n * Handles default dimensions and generates proper meta tags\n */\nexport function processOpenGraphMedia(\n  mediaType: \"image\" | \"video\" | \"audio\",\n  media: ReadonlyArray<OpenGraphMedia> = [],\n  defaults?: { width?: number; height?: number },\n) {\n  const tags: Array<{ property: string; content: string }> = [];\n\n  media.forEach((item) => {\n    // Main media URL\n    tags.push({\n      property: `og:${mediaType}`,\n      content: item.url,\n    });\n\n    // Alt text for images\n    if (item.alt) {\n      tags.push({\n        property: `og:${mediaType}:alt`,\n        content: item.alt,\n      });\n    }\n\n    // Secure URL\n    if (item.secureUrl) {\n      tags.push({\n        property: `og:${mediaType}:secure_url`,\n        content: item.secureUrl,\n      });\n    }\n\n    // Media type\n    if (item.type) {\n      tags.push({\n        property: `og:${mediaType}:type`,\n        content: item.type,\n      });\n    }\n\n    // Width\n    const width = item.width || defaults?.width;\n    if (width) {\n      tags.push({\n        property: `og:${mediaType}:width`,\n        content: width.toString(),\n      });\n    }\n\n    // Height\n    const height = item.height || defaults?.height;\n    if (height) {\n      tags.push({\n        property: `og:${mediaType}:height`,\n        content: height.toString(),\n      });\n    }\n  });\n\n  return tags;\n}\n\n/**\n * Build robots meta content string from properties\n */\nexport function buildRobotsContent(\n  noindex?: boolean,\n  nofollow?: boolean,\n  robotsProps?: {\n    nosnippet?: boolean;\n    maxSnippet?: number;\n    maxImagePreview?: string;\n    maxVideoPreview?: number;\n    noarchive?: boolean;\n    unavailableAfter?: string;\n    noimageindex?: boolean;\n    notranslate?: boolean;\n  },\n): string {\n  const parts: string[] = [];\n\n  // Index/follow directives\n  parts.push(noindex ? \"noindex\" : \"index\");\n  parts.push(nofollow ? \"nofollow\" : \"follow\");\n\n  // Additional robots properties\n  if (robotsProps) {\n    if (robotsProps.nosnippet) parts.push(\"nosnippet\");\n    if (robotsProps.maxSnippet !== undefined) {\n      parts.push(`max-snippet:${robotsProps.maxSnippet}`);\n    }\n    if (robotsProps.maxImagePreview) {\n      parts.push(`max-image-preview:${robotsProps.maxImagePreview}`);\n    }\n    if (robotsProps.noarchive) parts.push(\"noarchive\");\n    if (robotsProps.unavailableAfter) {\n      parts.push(`unavailable_after:${robotsProps.unavailableAfter}`);\n    }\n    if (robotsProps.noimageindex) parts.push(\"noimageindex\");\n    if (robotsProps.maxVideoPreview !== undefined) {\n      parts.push(`max-video-preview:${robotsProps.maxVideoPreview}`);\n    }\n    if (robotsProps.notranslate) parts.push(\"notranslate\");\n  }\n\n  return parts.join(\",\");\n}\n\n/**\n * Process title with template\n */\nexport function processTitle(\n  title?: string,\n  defaultTitle?: string,\n  titleTemplate?: string,\n): string | undefined {\n  let finalTitle = title || defaultTitle;\n\n  if (!finalTitle) return undefined;\n\n  if (title && titleTemplate) {\n    finalTitle = titleTemplate.replace(/%s/g, title);\n  }\n\n  return finalTitle;\n}\n\n/**\n * Generate unique key for meta/link tags\n */\nexport function generateTagKey(tag: {\n  name?: string;\n  property?: string;\n  httpEquiv?: string;\n  keyOverride?: string;\n}): string {\n  return tag.keyOverride || tag.name || tag.property || tag.httpEquiv || \"\";\n}\n"
  },
  {
    "path": "src/types/article.types.ts",
    "content": "import type { ImageObject, Organization, Person, Author } from \"./common.types\";\n\nexport type Publisher =\n  | string\n  | Organization\n  | Person\n  | Omit<Organization, \"@type\">\n  | Omit<Person, \"@type\">;\n\nexport interface ArticleBase {\n  headline: string;\n  url?: string;\n  author?: Author | Author[];\n  datePublished?: string;\n  dateModified?: string;\n  image?:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  publisher?: Publisher;\n  description?: string;\n  isAccessibleForFree?: boolean;\n  mainEntityOfPage?:\n    | string\n    | {\n        \"@type\": \"WebPage\";\n        \"@id\": string;\n      }\n    | {\n        \"@id\": string;\n      };\n}\n\nexport interface Article extends ArticleBase {\n  \"@type\": \"Article\";\n}\n\nexport interface NewsArticle extends ArticleBase {\n  \"@type\": \"NewsArticle\";\n}\n\nexport interface BlogPosting extends ArticleBase {\n  \"@type\": \"BlogPosting\";\n}\n\nexport interface Blog extends ArticleBase {\n  \"@type\": \"Blog\";\n}\n\nexport type ArticleJsonLdProps = (\n  | Omit<Article, \"@type\">\n  | Omit<NewsArticle, \"@type\">\n  | Omit<BlogPosting, \"@type\">\n  | Omit<Blog, \"@type\">\n) & {\n  type?: \"Article\" | \"NewsArticle\" | \"BlogPosting\" | \"Blog\";\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/breadcrumb.types.ts",
    "content": "// Type definitions for BreadcrumbList structured data\n\n// Flexible input type for breadcrumb items\nexport interface BreadcrumbListItem {\n  name: string;\n  item?: string | { \"@id\": string };\n}\n\n// Schema.org ListItem type\nexport interface ListItem {\n  \"@type\": \"ListItem\";\n  position: number;\n  name?: string;\n  item?: string | { \"@id\": string };\n}\n\n// Schema.org BreadcrumbList type\nexport interface BreadcrumbList {\n  \"@context\": \"https://schema.org\";\n  \"@type\": \"BreadcrumbList\";\n  itemListElement: ListItem[];\n}\n\n// Component props supporting both single and multiple trails\nexport type BreadcrumbJsonLdProps =\n  | {\n      items: BreadcrumbListItem[];\n      scriptId?: string;\n      scriptKey?: string;\n    }\n  | {\n      multipleTrails: BreadcrumbListItem[][];\n      scriptId?: string;\n      scriptKey?: string;\n    };\n"
  },
  {
    "path": "src/types/carousel.types.ts",
    "content": "import type { Course } from \"./course.types\";\nimport type { Movie } from \"./movie-carousel.types\";\nimport type { Recipe } from \"./recipe.types\";\nimport type { Restaurant } from \"./localbusiness.types\";\nimport type {\n  ImageObject,\n  AggregateRating,\n  VideoObject,\n  Review,\n  Person,\n  Organization,\n  GeoCoordinates,\n  OpeningHoursSpecification,\n} from \"./common.types\";\n\n// Content types supported in carousels\nexport type CarouselContentType = \"Course\" | \"Movie\" | \"Recipe\" | \"Restaurant\";\n\n// ListItem for ItemList (used in both summary and all-in-one patterns)\nexport interface CarouselListItem {\n  \"@type\": \"ListItem\";\n  position: number;\n  url?: string; // For summary page pattern\n  item?: Course | Movie | Recipe | Restaurant; // For all-in-one page pattern\n}\n\n// ItemList for carousel\nexport interface CarouselItemList {\n  \"@context\": \"https://schema.org\";\n  \"@type\": \"ItemList\";\n  itemListElement: CarouselListItem[];\n}\n\n// Summary page item - just URL with optional position\nexport type SummaryPageItem = string | { url: string; position?: number };\n\n// Flexible input types for each content type\nexport type CourseItem = Omit<Course, \"@type\" | \"provider\"> & {\n  provider?: string | Organization | Omit<Organization, \"@type\">;\n};\n\nexport type MovieItem = Omit<Movie, \"@type\" | \"image\"> & {\n  image:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  director?: string | Person | Omit<Person, \"@type\">;\n  review?: Review | Omit<Review, \"@type\">;\n  aggregateRating?: AggregateRating | Omit<AggregateRating, \"@type\">;\n};\n\nexport type RecipeItem = Omit<\n  Recipe,\n  \"@type\" | \"image\" | \"aggregateRating\" | \"video\"\n> & {\n  image:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  aggregateRating?: AggregateRating | Omit<AggregateRating, \"@type\">;\n  video?: VideoObject | Omit<VideoObject, \"@type\">;\n};\n\nexport type RestaurantItem = Omit<\n  Restaurant,\n  | \"@type\"\n  | \"image\"\n  | \"geo\"\n  | \"openingHoursSpecification\"\n  | \"review\"\n  | \"aggregateRating\"\n> & {\n  image?:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  geo?: GeoCoordinates | Omit<GeoCoordinates, \"@type\">;\n  openingHoursSpecification?:\n    | OpeningHoursSpecification\n    | Omit<OpeningHoursSpecification, \"@type\">\n    | OpeningHoursSpecification[]\n    | Omit<OpeningHoursSpecification, \"@type\">[];\n  review?: Review | Omit<Review, \"@type\"> | Review[] | Omit<Review, \"@type\">[];\n  aggregateRating?: AggregateRating | Omit<AggregateRating, \"@type\">;\n};\n\n// Base props\ninterface CarouselJsonLdBaseProps {\n  scriptId?: string;\n  scriptKey?: string;\n}\n\n// Summary page pattern props\ninterface SummaryPageProps extends CarouselJsonLdBaseProps {\n  urls: SummaryPageItem[];\n}\n\n// All-in-one page pattern props for each content type\ninterface CourseCarouselProps extends CarouselJsonLdBaseProps {\n  contentType: \"Course\";\n  items: CourseItem[];\n}\n\ninterface MovieCarouselProps extends CarouselJsonLdBaseProps {\n  contentType: \"Movie\";\n  items: MovieItem[];\n}\n\ninterface RecipeCarouselProps extends CarouselJsonLdBaseProps {\n  contentType: \"Recipe\";\n  items: RecipeItem[];\n}\n\ninterface RestaurantCarouselProps extends CarouselJsonLdBaseProps {\n  contentType: \"Restaurant\";\n  items: RestaurantItem[];\n}\n\n// Component props supporting both patterns\nexport type CarouselJsonLdProps =\n  | SummaryPageProps\n  | CourseCarouselProps\n  | MovieCarouselProps\n  | RecipeCarouselProps\n  | RestaurantCarouselProps;\n"
  },
  {
    "path": "src/types/claimreview.types.ts",
    "content": "import type { Organization, Author, Rating } from \"./common.types\";\n\n// Extended Rating type for ClaimReview with required alternateName\nexport interface ClaimReviewRating extends Rating {\n  alternateName: string;\n  name?: string;\n}\n\n// CreativeWork type for appearance/firstAppearance\nexport interface ClaimCreativeWork {\n  \"@type\": string;\n  \"@id\"?: string;\n  url?: string;\n  headline?: string;\n  datePublished?: string;\n  author?: Author;\n  image?: string;\n  publisher?: Organization | Omit<Organization, \"@type\">;\n}\n\n// Claim type\nexport interface Claim {\n  \"@type\": \"Claim\";\n  appearance?:\n    | string\n    | ClaimCreativeWork\n    | Omit<ClaimCreativeWork, \"@type\">\n    | (string | ClaimCreativeWork | Omit<ClaimCreativeWork, \"@type\">)[];\n  author?: Author;\n  datePublished?: string;\n  firstAppearance?:\n    | string\n    | ClaimCreativeWork\n    | Omit<ClaimCreativeWork, \"@type\">;\n}\n\n// ClaimReview type\nexport interface ClaimReview {\n  \"@type\": \"ClaimReview\";\n  claimReviewed: string;\n  reviewRating: ClaimReviewRating | Omit<ClaimReviewRating, \"@type\">;\n  url: string;\n  author?: Author;\n  itemReviewed?: Claim | Omit<Claim, \"@type\">;\n}\n\n// Component props type\nexport type ClaimReviewJsonLdProps = Omit<ClaimReview, \"@type\"> & {\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/common.types.ts",
    "content": "// Base Schema.org types (e.g., Thing, Person)\n// This file will contain common TypeScript definitions based on Schema.org.\n\nexport interface Thing {\n  name?: string;\n  description?: string;\n  url?: string;\n  image?: string;\n}\n\nexport interface ImageObject {\n  \"@type\": \"ImageObject\";\n  url: string;\n  width?: number;\n  height?: number;\n  caption?: string;\n}\n\nexport interface Person extends Thing {\n  \"@type\": \"Person\";\n  familyName?: string;\n  givenName?: string;\n  additionalName?: string;\n  alternateName?: string;\n  identifier?: string;\n  interactionStatistic?:\n    | InteractionCounter\n    | InteractionCounter[]\n    | Omit<InteractionCounter, \"@type\">\n    | Omit<InteractionCounter, \"@type\">[];\n  agentInteractionStatistic?:\n    | InteractionCounter\n    | Omit<InteractionCounter, \"@type\">;\n  sameAs?: string | string[];\n}\n\nexport interface Organization extends Thing {\n  \"@type\": \"Organization\";\n  logo?: string | ImageObject | Omit<ImageObject, \"@type\">;\n  sameAs?: string | string[];\n  address?:\n    | string\n    | PostalAddress\n    | Omit<PostalAddress, \"@type\">\n    | (string | PostalAddress | Omit<PostalAddress, \"@type\">)[];\n  contactPoint?:\n    | ContactPoint\n    | Omit<ContactPoint, \"@type\">\n    | ContactPoint[]\n    | Omit<ContactPoint, \"@type\">[];\n  telephone?: string;\n  email?: string;\n  alternateName?: string;\n  foundingDate?: string;\n  legalName?: string;\n  taxID?: string;\n  vatID?: string;\n  duns?: string;\n  leiCode?: string;\n  naics?: string;\n  globalLocationNumber?: string;\n  iso6523Code?: string;\n  numberOfEmployees?:\n    | number\n    | QuantitativeValue\n    | Omit<QuantitativeValue, \"@type\">;\n  review?: Review | Omit<Review, \"@type\"> | Review[] | Omit<Review, \"@type\">[];\n  aggregateRating?: AggregateRating | Omit<AggregateRating, \"@type\">;\n  identifier?: string;\n  interactionStatistic?:\n    | InteractionCounter\n    | InteractionCounter[]\n    | Omit<InteractionCounter, \"@type\">\n    | Omit<InteractionCounter, \"@type\">[];\n  agentInteractionStatistic?:\n    | InteractionCounter\n    | Omit<InteractionCounter, \"@type\">;\n}\n\nexport interface PostalAddress {\n  \"@type\": \"PostalAddress\";\n  streetAddress?: string;\n  addressLocality?: string;\n  addressRegion?: string;\n  postalCode?: string;\n  addressCountry?: string;\n}\n\nexport interface Place {\n  \"@type\": \"Place\";\n  name?: string;\n  address?: PostalAddress | Omit<PostalAddress, \"@type\">;\n}\n\nexport interface ContactPoint {\n  \"@type\": \"ContactPoint\";\n  contactType?: string;\n  telephone?: string;\n  email?: string;\n}\n\nexport interface QuantitativeValue {\n  \"@type\": \"QuantitativeValue\";\n  value?: number | string;\n  minValue?: number;\n  maxValue?: number;\n  unitText?: string;\n  unitCode?: string;\n  valueReference?: QuantitativeValue | Omit<QuantitativeValue, \"@type\">;\n}\n\nexport interface SimpleMonetaryAmount {\n  \"@type\": \"MonetaryAmount\";\n  value: number | string;\n  currency: string;\n}\n\nexport interface MerchantReturnPolicySeasonalOverride {\n  \"@type\": \"MerchantReturnPolicySeasonalOverride\";\n  startDate?: string;\n  endDate?: string;\n  returnPolicyCategory?: string;\n  merchantReturnDays?: number | string;\n}\n\nexport interface MerchantReturnPolicy {\n  \"@type\": \"MerchantReturnPolicy\";\n  // Option A: Detailed properties\n  applicableCountry?: string | string[];\n  returnPolicyCountry?: string | string[];\n  returnPolicyCategory?: string;\n  merchantReturnDays?: number;\n  returnMethod?: string | string[];\n  returnFees?: string;\n  returnShippingFeesAmount?:\n    | SimpleMonetaryAmount\n    | Omit<SimpleMonetaryAmount, \"@type\">;\n  refundType?: string | string[];\n  restockingFee?:\n    | number\n    | SimpleMonetaryAmount\n    | Omit<SimpleMonetaryAmount, \"@type\">;\n  returnLabelSource?: string;\n  itemCondition?: string | string[];\n  // Customer remorse specific properties\n  customerRemorseReturnFees?: string;\n  customerRemorseReturnShippingFeesAmount?:\n    | SimpleMonetaryAmount\n    | Omit<SimpleMonetaryAmount, \"@type\">;\n  customerRemorseReturnLabelSource?: string;\n  // Item defect specific properties\n  itemDefectReturnFees?: string;\n  itemDefectReturnShippingFeesAmount?:\n    | SimpleMonetaryAmount\n    | Omit<SimpleMonetaryAmount, \"@type\">;\n  itemDefectReturnLabelSource?: string;\n  // Seasonal override\n  returnPolicySeasonalOverride?:\n    | MerchantReturnPolicySeasonalOverride\n    | Omit<MerchantReturnPolicySeasonalOverride, \"@type\">\n    | MerchantReturnPolicySeasonalOverride[]\n    | Omit<MerchantReturnPolicySeasonalOverride, \"@type\">[];\n  // Option B: Link to policy\n  merchantReturnLink?: string;\n}\n\nexport type PriceTypeEnumeration =\n  | \"https://schema.org/StrikethroughPrice\"\n  | \"https://schema.org/ListPrice\"\n  | \"StrikethroughPrice\"\n  | \"ListPrice\";\n\nexport interface CreditCard {\n  \"@type\": \"CreditCard\";\n  name: string;\n}\n\nexport interface UnitPriceSpecification {\n  \"@type\": \"UnitPriceSpecification\";\n  price?: number | string;\n  priceCurrency?: string;\n  billingDuration?: number;\n  billingIncrement?: number;\n  unitCode?: string;\n  priceType?: PriceTypeEnumeration;\n  validForMemberTier?:\n    | MemberProgramTier\n    | Omit<MemberProgramTier, \"@type\">\n    | (MemberProgramTier | Omit<MemberProgramTier, \"@type\">)[];\n  membershipPointsEarned?: number;\n  referenceQuantity?: QuantitativeValue | Omit<QuantitativeValue, \"@type\">;\n}\n\nexport type TierRequirement =\n  | CreditCard\n  | SimpleMonetaryAmount\n  | UnitPriceSpecification\n  | string\n  | Omit<CreditCard, \"@type\">\n  | Omit<SimpleMonetaryAmount, \"@type\">\n  | Omit<UnitPriceSpecification, \"@type\">;\n\nexport type TierBenefit =\n  | \"TierBenefitLoyaltyPoints\"\n  | \"TierBenefitLoyaltyPrice\"\n  | \"https://schema.org/TierBenefitLoyaltyPoints\"\n  | \"https://schema.org/TierBenefitLoyaltyPrice\";\n\nexport interface MemberProgramTier {\n  \"@type\": \"MemberProgramTier\";\n  \"@id\"?: string;\n  name: string;\n  url?: string;\n  hasTierBenefit: TierBenefit | TierBenefit[];\n  hasTierRequirement?: TierRequirement;\n  membershipPointsEarned?:\n    | number\n    | QuantitativeValue\n    | Omit<QuantitativeValue, \"@type\">;\n}\n\nexport interface MemberProgram {\n  \"@type\": \"MemberProgram\";\n  name: string;\n  description: string;\n  url?: string;\n  hasTiers:\n    | MemberProgramTier\n    | Omit<MemberProgramTier, \"@type\">\n    | (MemberProgramTier | Omit<MemberProgramTier, \"@type\">)[];\n}\n\nexport type Author =\n  | string\n  | Person\n  | Organization\n  | Omit<Person, \"@type\">\n  | Omit<Organization, \"@type\">;\n\nexport interface GeoCoordinates {\n  \"@type\": \"GeoCoordinates\";\n  latitude: number;\n  longitude: number;\n}\n\nexport interface Rating {\n  \"@type\": \"Rating\";\n  ratingValue: number | string;\n  bestRating?: number | string;\n  worstRating?: number | string;\n}\n\nexport interface Review {\n  \"@type\": \"Review\";\n  reviewRating?: Rating | Omit<Rating, \"@type\">;\n  author?: Author;\n  reviewBody?: string;\n  datePublished?: string;\n}\n\nexport interface AggregateRating {\n  \"@type\": \"AggregateRating\";\n  ratingValue: number;\n  ratingCount?: number;\n  reviewCount?: number;\n  bestRating?: number;\n  worstRating?: number;\n}\n\nexport interface OpeningHoursSpecification {\n  \"@type\": \"OpeningHoursSpecification\";\n  dayOfWeek: string | string[];\n  opens: string;\n  closes: string;\n  validFrom?: string;\n  validThrough?: string;\n}\n\nexport interface VideoObject {\n  \"@type\": \"VideoObject\";\n  name: string;\n  description: string;\n  thumbnailUrl: string | string[];\n  contentUrl?: string;\n  embedUrl?: string;\n  uploadDate: string;\n  duration?: string;\n  expires?: string;\n}\n\nexport interface InteractionCounter {\n  \"@type\": \"InteractionCounter\";\n  interactionType: string;\n  userInteractionCount: number;\n}\n\nexport interface Brand {\n  \"@type\": \"Brand\";\n  name?: string;\n}\n\nexport interface BedDetails {\n  \"@type\": \"BedDetails\";\n  numberOfBeds?: number;\n  typeOfBed?: string;\n}\n\nexport interface LocationFeatureSpecification {\n  \"@type\": \"LocationFeatureSpecification\";\n  name: string;\n  value: boolean | string;\n}\n\nexport interface Accommodation {\n  \"@type\": \"Accommodation\";\n  additionalType?: string;\n  bed?:\n    | BedDetails\n    | Omit<BedDetails, \"@type\">\n    | (BedDetails | Omit<BedDetails, \"@type\">)[];\n  occupancy?: QuantitativeValue | Omit<QuantitativeValue, \"@type\">;\n  amenityFeature?:\n    | LocationFeatureSpecification\n    | Omit<LocationFeatureSpecification, \"@type\">\n    | (\n        | LocationFeatureSpecification\n        | Omit<LocationFeatureSpecification, \"@type\">\n      )[];\n  floorSize?: QuantitativeValue | Omit<QuantitativeValue, \"@type\">;\n  numberOfBathroomsTotal?: number;\n  numberOfBedrooms?: number;\n  numberOfRooms?: number;\n  petsAllowed?: boolean;\n  smokingAllowed?: boolean;\n}\n\nexport interface Certification {\n  \"@type\": \"Certification\";\n  issuedBy:\n    | Organization\n    | Omit<Organization, \"@type\">\n    | { \"@type\"?: \"Organization\"; name: string };\n  name: string;\n  url?: string;\n  certificationIdentification?: string;\n  certificationRating?: Rating | Omit<Rating, \"@type\">;\n}\n\nexport interface PeopleAudience {\n  \"@type\": \"PeopleAudience\";\n  suggestedGender?:\n    | \"male\"\n    | \"female\"\n    | \"unisex\"\n    | \"https://schema.org/Male\"\n    | \"https://schema.org/Female\";\n  suggestedMinAge?: number;\n  suggestedMaxAge?: number;\n  suggestedAge?: QuantitativeValue | Omit<QuantitativeValue, \"@type\">;\n}\n\nexport interface SizeSpecification {\n  \"@type\": \"SizeSpecification\";\n  name?: string;\n  sizeGroup?:\n    | \"WearableSizeGroupRegular\"\n    | \"WearableSizeGroupPetite\"\n    | \"WearableSizeGroupPlus\"\n    | \"WearableSizeGroupTall\"\n    | \"WearableSizeGroupBig\"\n    | \"WearableSizeGroupMaternity\"\n    | \"https://schema.org/WearableSizeGroupRegular\"\n    | \"https://schema.org/WearableSizeGroupPetite\"\n    | \"https://schema.org/WearableSizeGroupPlus\"\n    | \"https://schema.org/WearableSizeGroupTall\"\n    | \"https://schema.org/WearableSizeGroupBig\"\n    | \"https://schema.org/WearableSizeGroupMaternity\"\n    | \"regular\"\n    | \"petite\"\n    | \"plus\"\n    | \"tall\"\n    | \"big\"\n    | \"maternity\";\n  sizeSystem?:\n    | \"WearableSizeSystemAU\"\n    | \"WearableSizeSystemBR\"\n    | \"WearableSizeSystemCN\"\n    | \"WearableSizeSystemDE\"\n    | \"WearableSizeSystemEurope\"\n    | \"WearableSizeSystemFR\"\n    | \"WearableSizeSystemIT\"\n    | \"WearableSizeSystemJP\"\n    | \"WearableSizeSystemMX\"\n    | \"WearableSizeSystemUK\"\n    | \"WearableSizeSystemUS\"\n    | \"https://schema.org/WearableSizeSystemAU\"\n    | \"https://schema.org/WearableSizeSystemBR\"\n    | \"https://schema.org/WearableSizeSystemCN\"\n    | \"https://schema.org/WearableSizeSystemDE\"\n    | \"https://schema.org/WearableSizeSystemEurope\"\n    | \"https://schema.org/WearableSizeSystemFR\"\n    | \"https://schema.org/WearableSizeSystemIT\"\n    | \"https://schema.org/WearableSizeSystemJP\"\n    | \"https://schema.org/WearableSizeSystemMX\"\n    | \"https://schema.org/WearableSizeSystemUK\"\n    | \"https://schema.org/WearableSizeSystemUS\"\n    | \"AU\"\n    | \"BR\"\n    | \"CN\"\n    | \"DE\"\n    | \"EU\"\n    | \"FR\"\n    | \"IT\"\n    | \"JP\"\n    | \"MX\"\n    | \"UK\"\n    | \"US\";\n}\n\nexport interface ThreeDModel {\n  \"@type\": \"3DModel\";\n  encoding?: {\n    \"@type\"?: \"MediaObject\";\n    contentUrl: string;\n  };\n}\n\nexport interface DefinedRegion {\n  \"@type\": \"DefinedRegion\";\n  addressCountry: string;\n  addressRegion?: string | string[];\n  postalCode?: string | string[];\n}\n\nexport interface ShippingDeliveryTime {\n  \"@type\": \"ShippingDeliveryTime\";\n  handlingTime?: QuantitativeValue | Omit<QuantitativeValue, \"@type\">;\n  transitTime?: QuantitativeValue | Omit<QuantitativeValue, \"@type\">;\n}\n\nexport interface OfferShippingDetails {\n  \"@type\": \"OfferShippingDetails\";\n  shippingRate?: SimpleMonetaryAmount | Omit<SimpleMonetaryAmount, \"@type\">;\n  shippingDestination?:\n    | DefinedRegion\n    | Omit<DefinedRegion, \"@type\">\n    | (DefinedRegion | Omit<DefinedRegion, \"@type\">)[];\n  deliveryTime?: ShippingDeliveryTime | Omit<ShippingDeliveryTime, \"@type\">;\n  doesNotShip?: boolean;\n}\n"
  },
  {
    "path": "src/types/course.types.ts",
    "content": "import type { Organization } from \"./common.types\";\n\n// Provider can be a string or Organization\nexport type Provider = string | Organization | Omit<Organization, \"@type\">;\n\n// Course schema type\nexport interface Course {\n  \"@type\": \"Course\";\n  name: string;\n  description: string;\n  url?: string;\n  provider?: Organization;\n}\n\n// Flexible input type for courses\nexport type CourseListItem = Omit<Course, \"@type\" | \"provider\"> & {\n  provider?: Provider;\n};\n\n// ListItem for ItemList (used in both summary and all-in-one patterns)\nexport interface CourseListItemSchema {\n  \"@type\": \"ListItem\";\n  position: number;\n  url?: string; // For summary page pattern\n  item?: Course; // For all-in-one page pattern\n}\n\n// ItemList for course list\nexport interface CourseItemList {\n  \"@context\": \"https://schema.org\";\n  \"@type\": \"ItemList\";\n  itemListElement: CourseListItemSchema[];\n}\n\n// Summary page item - just URL with optional position\nexport type SummaryPageItem = string | { url: string; position?: number };\n\n// Base props\ninterface CourseJsonLdBaseProps {\n  scriptId?: string;\n  scriptKey?: string;\n}\n\n// Single course props\ninterface SingleCourseProps extends CourseJsonLdBaseProps {\n  type?: \"single\";\n  name: string;\n  description: string;\n  url?: string;\n  provider?: Provider;\n}\n\n// Summary page props\ninterface CourseListSummaryProps extends CourseJsonLdBaseProps {\n  type: \"list\";\n  urls: SummaryPageItem[];\n}\n\n// All-in-one page props\ninterface CourseListAllInOneProps extends CourseJsonLdBaseProps {\n  type: \"list\";\n  courses: CourseListItem[];\n}\n\n// Component props supporting both single course and list patterns\nexport type CourseJsonLdProps =\n  | SingleCourseProps\n  | CourseListSummaryProps\n  | CourseListAllInOneProps;\n"
  },
  {
    "path": "src/types/creativework.types.ts",
    "content": "import type { ImageObject, Organization, Person, Author } from \"./common.types\";\n\n// WebPageElement for marking paywalled sections\nexport interface WebPageElement {\n  \"@type\": \"WebPageElement\";\n  isAccessibleForFree: boolean;\n  cssSelector: string;\n}\n\n// Publisher type (same as Article)\nexport type Publisher =\n  | string\n  | Organization\n  | Person\n  | Omit<Organization, \"@type\">\n  | Omit<Person, \"@type\">;\n\n// Base interface with common properties for all CreativeWork types\nexport interface CreativeWorkBase {\n  headline?: string;\n  name?: string; // Alternative to headline for some types\n  url?: string;\n  author?: Author | Author[];\n  datePublished?: string;\n  dateModified?: string;\n  image?:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  publisher?: Publisher;\n  description?: string;\n  isAccessibleForFree?: boolean;\n  hasPart?:\n    | WebPageElement\n    | Omit<WebPageElement, \"@type\">\n    | (WebPageElement | Omit<WebPageElement, \"@type\">)[];\n  mainEntityOfPage?:\n    | string\n    | {\n        \"@type\": \"WebPage\";\n        \"@id\": string;\n      }\n    | {\n        \"@id\": string;\n      };\n}\n\n// Specific schema type interfaces for all supported CreativeWork types\nexport interface CreativeWork extends CreativeWorkBase {\n  \"@type\": \"CreativeWork\";\n}\n\nexport interface Article extends CreativeWorkBase {\n  \"@type\": \"Article\";\n  headline: string; // Required for Article\n}\n\nexport interface NewsArticle extends CreativeWorkBase {\n  \"@type\": \"NewsArticle\";\n  headline: string; // Required for NewsArticle\n}\n\nexport interface Blog extends CreativeWorkBase {\n  \"@type\": \"Blog\";\n}\n\nexport interface BlogPosting extends CreativeWorkBase {\n  \"@type\": \"BlogPosting\";\n  headline: string; // Required for BlogPosting\n}\n\nexport interface Comment extends CreativeWorkBase {\n  \"@type\": \"Comment\";\n  text?: string; // Comment-specific property\n}\n\nexport interface Course extends CreativeWorkBase {\n  \"@type\": \"Course\";\n  name: string; // Required for Course\n  provider?: Publisher;\n}\n\nexport interface HowTo extends CreativeWorkBase {\n  \"@type\": \"HowTo\";\n  name: string; // Required for HowTo\n}\n\nexport interface Message extends CreativeWorkBase {\n  \"@type\": \"Message\";\n}\n\nexport interface Review extends CreativeWorkBase {\n  \"@type\": \"Review\";\n  itemReviewed?: string | { name: string; \"@type\"?: string };\n  reviewRating?: {\n    \"@type\"?: \"Rating\";\n    ratingValue: number;\n    bestRating?: number;\n    worstRating?: number;\n  };\n}\n\nexport interface WebPage extends CreativeWorkBase {\n  \"@type\": \"WebPage\";\n}\n\n// Component props type - include all possible properties from all types\nexport type CreativeWorkJsonLdProps = {\n  // Common properties from CreativeWorkBase\n  headline?: string;\n  name?: string;\n  url?: string;\n  author?: Author | Author[];\n  datePublished?: string;\n  dateModified?: string;\n  image?:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  publisher?: Publisher;\n  description?: string;\n  isAccessibleForFree?: boolean;\n  hasPart?:\n    | WebPageElement\n    | Omit<WebPageElement, \"@type\">\n    | (WebPageElement | Omit<WebPageElement, \"@type\">)[];\n  mainEntityOfPage?:\n    | string\n    | {\n        \"@type\": \"WebPage\";\n        \"@id\": string;\n      }\n    | {\n        \"@id\": string;\n      };\n\n  // Type-specific properties\n  text?: string; // For Comment\n  provider?: Publisher; // For Course\n  itemReviewed?: string | { name: string; \"@type\"?: string }; // For Review\n  reviewRating?: {\n    \"@type\"?: \"Rating\";\n    ratingValue: number;\n    bestRating?: number;\n    worstRating?: number;\n  }; // For Review\n\n  // Component control properties\n  type?:\n    | \"CreativeWork\"\n    | \"Article\"\n    | \"NewsArticle\"\n    | \"Blog\"\n    | \"BlogPosting\"\n    | \"Comment\"\n    | \"Course\"\n    | \"HowTo\"\n    | \"Message\"\n    | \"Review\"\n    | \"WebPage\";\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/dataset.types.ts",
    "content": "import type { Author, GeoCoordinates } from \"./common.types\";\n\n// Base types needed for Dataset\n\nexport interface GeoShape {\n  \"@type\": \"GeoShape\";\n  box?: string;\n  circle?: string;\n  line?: string;\n  polygon?: string;\n}\n\nexport interface PropertyValue {\n  \"@type\": \"PropertyValue\";\n  name?: string;\n  value?: string;\n  propertyID?: string;\n}\n\nexport interface CreativeWork {\n  \"@type\": \"CreativeWork\";\n  name?: string;\n  url?: string;\n  identifier?: string;\n}\n\n// Place type specifically for Dataset (more comprehensive than event Place)\nexport interface DatasetPlace {\n  \"@type\": \"Place\";\n  name?: string;\n  geo?:\n    | GeoCoordinates\n    | GeoShape\n    | Omit<GeoCoordinates, \"@type\">\n    | Omit<GeoShape, \"@type\">;\n}\n\n// DataDownload for distribution\nexport interface DataDownload {\n  \"@type\": \"DataDownload\";\n  contentUrl: string;\n  encodingFormat?: string;\n  contentSize?: string;\n  name?: string;\n  description?: string;\n}\n\n// DataCatalog for includedInDataCatalog\nexport interface DataCatalog {\n  \"@type\": \"DataCatalog\";\n  name: string;\n  description?: string;\n  url?: string;\n  hasPart?: Dataset | Dataset[];\n}\n\n// Main Dataset interface\nexport interface Dataset {\n  \"@type\": \"Dataset\";\n  name: string;\n  description: string;\n  url?: string;\n  sameAs?: string | string[];\n  identifier?:\n    | string\n    | PropertyValue\n    | Omit<PropertyValue, \"@type\">\n    | (string | PropertyValue | Omit<PropertyValue, \"@type\">)[];\n  keywords?: string | string[];\n  license?: string | CreativeWork | Omit<CreativeWork, \"@type\">;\n  isAccessibleForFree?: boolean;\n  hasPart?: Dataset | Dataset[];\n  isPartOf?: string | Dataset;\n  creator?: Author | Author[];\n  funder?: Author | Author[];\n  includedInDataCatalog?: DataCatalog | Omit<DataCatalog, \"@type\">;\n  distribution?:\n    | DataDownload\n    | Omit<DataDownload, \"@type\">\n    | (DataDownload | Omit<DataDownload, \"@type\">)[];\n  temporalCoverage?: string;\n  spatialCoverage?: string | DatasetPlace | Omit<DatasetPlace, \"@type\">;\n  alternateName?: string | string[];\n  citation?:\n    | string\n    | CreativeWork\n    | Omit<CreativeWork, \"@type\">\n    | (string | CreativeWork | Omit<CreativeWork, \"@type\">)[];\n  measurementTechnique?: string | string[];\n  variableMeasured?:\n    | string\n    | PropertyValue\n    | Omit<PropertyValue, \"@type\">\n    | (string | PropertyValue | Omit<PropertyValue, \"@type\">)[];\n  version?: string | number;\n}\n\n// Component props type\nexport type DatasetJsonLdProps = Omit<Dataset, \"@type\"> & {\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/discussionforum.types.ts",
    "content": "import type {\n  Author,\n  ImageObject,\n  VideoObject,\n  InteractionCounter,\n} from \"./common.types\";\n\n// WebPage type for sharedContent\nexport interface WebPage {\n  \"@type\": \"WebPage\";\n  url: string;\n  name?: string;\n  description?: string;\n}\n\n// CreativeWork can be used for isPartOf\nexport interface CreativeWork {\n  \"@type\": \"CreativeWork\";\n  name?: string;\n  url?: string;\n}\n\n// SharedContent can be various types\nexport type SharedContent =\n  | string\n  | WebPage\n  | ImageObject\n  | VideoObject\n  | Omit<WebPage, \"@type\">\n  | Omit<ImageObject, \"@type\">\n  | Omit<VideoObject, \"@type\">;\n\n// Comment interface with nested comment support\nexport interface Comment {\n  \"@type\": \"Comment\";\n  author: Author;\n  datePublished: string;\n  text?: string;\n  image?: string | ImageObject | Omit<ImageObject, \"@type\">;\n  video?: VideoObject | Omit<VideoObject, \"@type\">;\n  comment?: (Comment | Omit<Comment, \"@type\">)[];\n  creativeWorkStatus?: string;\n  dateModified?: string;\n  interactionStatistic?:\n    | InteractionCounter\n    | Omit<InteractionCounter, \"@type\">\n    | (InteractionCounter | Omit<InteractionCounter, \"@type\">)[];\n  sharedContent?: SharedContent;\n  url?: string;\n}\n\n// Base interface for both DiscussionForumPosting and SocialMediaPosting\nexport interface DiscussionForumPostingBase {\n  headline?: string;\n  text?: string;\n  image?:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  video?: VideoObject | Omit<VideoObject, \"@type\">;\n  author: Author | Author[];\n  datePublished: string;\n  dateModified?: string;\n  url?: string;\n  comment?: (Comment | Omit<Comment, \"@type\">)[];\n  creativeWorkStatus?: string;\n  interactionStatistic?:\n    | InteractionCounter\n    | Omit<InteractionCounter, \"@type\">\n    | (InteractionCounter | Omit<InteractionCounter, \"@type\">)[];\n  isPartOf?: string | CreativeWork | Omit<CreativeWork, \"@type\">;\n  sharedContent?: SharedContent;\n}\n\n// Specific schema types\nexport interface DiscussionForumPosting extends DiscussionForumPostingBase {\n  \"@type\": \"DiscussionForumPosting\";\n}\n\nexport interface SocialMediaPosting extends DiscussionForumPostingBase {\n  \"@type\": \"SocialMediaPosting\";\n}\n\n// Component props type\nexport type DiscussionForumPostingJsonLdProps = (\n  | Omit<DiscussionForumPosting, \"@type\">\n  | Omit<SocialMediaPosting, \"@type\">\n) & {\n  type?: \"DiscussionForumPosting\" | \"SocialMediaPosting\";\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/employer-aggregate-rating.types.ts",
    "content": "import type { Organization } from \"./common.types\";\n\n// EmployerAggregateRating specific to hiring organizations\nexport interface EmployerAggregateRating {\n  \"@type\": \"EmployerAggregateRating\";\n  itemReviewed: Organization;\n  ratingValue: number | string;\n  ratingCount?: number;\n  reviewCount?: number;\n  bestRating?: number;\n  worstRating?: number;\n}\n\n// Component props type\nexport type EmployerAggregateRatingJsonLdProps = Omit<\n  EmployerAggregateRating,\n  \"@type\" | \"itemReviewed\"\n> & {\n  itemReviewed: string | Organization | Omit<Organization, \"@type\">;\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/event.types.ts",
    "content": "import type {\n  ImageObject,\n  Person,\n  Organization,\n  PostalAddress,\n  Thing,\n} from \"./common.types\";\n\n// Place type for event location\nexport interface Place {\n  \"@type\": \"Place\";\n  name?: string;\n  address: PostalAddress | Omit<PostalAddress, \"@type\">;\n}\n\n// Offer type for ticket information\nexport interface Offer {\n  \"@type\": \"Offer\";\n  url?: string;\n  price?: number;\n  priceCurrency?: string;\n  availability?: string;\n  validFrom?: string;\n}\n\n// PerformingGroup type for performers\nexport interface PerformingGroup extends Thing {\n  \"@type\": \"PerformingGroup\";\n}\n\n// Performer can be Person or PerformingGroup\nexport type Performer =\n  | string\n  | Person\n  | PerformingGroup\n  | Omit<Person, \"@type\">\n  | Omit<PerformingGroup, \"@type\">;\n\n// Organizer can be Person or Organization\nexport type Organizer =\n  | string\n  | Person\n  | Organization\n  | Omit<Person, \"@type\">\n  | Omit<Organization, \"@type\">;\n\n// Event status types\nexport type EventStatusType =\n  | \"https://schema.org/EventScheduled\"\n  | \"https://schema.org/EventCancelled\"\n  | \"https://schema.org/EventPostponed\"\n  | \"https://schema.org/EventRescheduled\";\n\n// Base event interface\nexport interface EventBase {\n  name: string;\n  startDate: string;\n  location: string | Place | Omit<Place, \"@type\">;\n  endDate?: string;\n  description?: string;\n  eventStatus?: EventStatusType;\n  image?:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  offers?: Offer | Omit<Offer, \"@type\"> | Offer[] | Omit<Offer, \"@type\">[];\n  performer?: Performer | Performer[];\n  organizer?: Organizer;\n  previousStartDate?: string | string[];\n  url?: string;\n}\n\n// Standard Event type\nexport interface Event extends EventBase {\n  \"@type\": \"Event\";\n}\n\n// Component props\nexport type EventJsonLdProps = Omit<Event, \"@type\"> & {\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/faq.types.ts",
    "content": "// Type definitions for FAQ structured data based on Schema.org FAQPage\n\n// Answer type according to Schema.org\nexport interface Answer {\n  \"@type\": \"Answer\";\n  text: string;\n}\n\n// Question type according to Schema.org\nexport interface Question {\n  \"@type\": \"Question\";\n  name: string;\n  acceptedAnswer: Answer;\n}\n\n// FAQPage type according to Schema.org\nexport interface FAQPage {\n  \"@context\": \"https://schema.org\";\n  \"@type\": \"FAQPage\";\n  mainEntity: Question[];\n}\n\n// Flexible input type for questions - support both simple and complex formats\nexport type QuestionInput =\n  | string // Just the question text, answer will be empty\n  | {\n      question: string;\n      answer: string;\n    }\n  | {\n      name: string;\n      acceptedAnswer: string | Answer;\n    }\n  | Omit<Question, \"@type\">;\n\n// Component props\nexport interface FAQJsonLdProps {\n  questions: QuestionInput[];\n  scriptId?: string;\n  scriptKey?: string;\n}\n"
  },
  {
    "path": "src/types/howto.types.ts",
    "content": "import type {\n  ImageObject,\n  QuantitativeValue,\n  SimpleMonetaryAmount,\n  VideoObject,\n} from \"./common.types\";\n\n/**\n * HowToSupply - A supply consumed when performing instructions\n * @see https://schema.org/HowToSupply\n */\nexport interface HowToSupply {\n  \"@type\": \"HowToSupply\";\n  name: string;\n  image?: string | ImageObject | Omit<ImageObject, \"@type\">;\n  requiredQuantity?:\n    | number\n    | string\n    | QuantitativeValue\n    | Omit<QuantitativeValue, \"@type\">;\n  estimatedCost?:\n    | string\n    | SimpleMonetaryAmount\n    | Omit<SimpleMonetaryAmount, \"@type\">;\n}\n\n/**\n * HowToTool - An object used (but not consumed) when performing instructions\n * @see https://schema.org/HowToTool\n */\nexport interface HowToTool {\n  \"@type\": \"HowToTool\";\n  name: string;\n  image?: string | ImageObject | Omit<ImageObject, \"@type\">;\n  requiredQuantity?:\n    | number\n    | string\n    | QuantitativeValue\n    | Omit<QuantitativeValue, \"@type\">;\n}\n\n/**\n * HowToDirection - A direction or instruction within a HowToStep\n * @see https://schema.org/HowToDirection\n */\nexport interface HowToDirection {\n  \"@type\": \"HowToDirection\";\n  text: string;\n  position?: number;\n  beforeMedia?: string | ImageObject | Omit<ImageObject, \"@type\">;\n  afterMedia?: string | ImageObject | Omit<ImageObject, \"@type\">;\n  duringMedia?: string | ImageObject | Omit<ImageObject, \"@type\">;\n}\n\n/**\n * HowToTip - A tip or suggestion within a HowToStep\n * @see https://schema.org/HowToTip\n */\nexport interface HowToTip {\n  \"@type\": \"HowToTip\";\n  text: string;\n  position?: number;\n}\n\n/**\n * HowToStep - A step in a HowTo guide\n * @see https://schema.org/HowToStep\n */\nexport interface HowToStep {\n  \"@type\": \"HowToStep\";\n  name?: string;\n  text?: string;\n  url?: string;\n  image?: string | ImageObject | Omit<ImageObject, \"@type\">;\n  position?: number;\n  itemListElement?: (\n    | HowToDirection\n    | HowToTip\n    | Omit<HowToDirection, \"@type\">\n    | Omit<HowToTip, \"@type\">\n  )[];\n}\n\n/**\n * HowToSection - A section of steps within a HowTo guide\n * @see https://schema.org/HowToSection\n */\nexport interface HowToSection {\n  \"@type\": \"HowToSection\";\n  name: string;\n  position?: number;\n  itemListElement: (HowToStep | Omit<HowToStep, \"@type\">)[];\n}\n\n/**\n * Step type union - represents all valid step types\n */\nexport type Step =\n  | string\n  | HowToStep\n  | HowToSection\n  | Omit<HowToStep, \"@type\">\n  | Omit<HowToSection, \"@type\">;\n\n/**\n * Supply type union - flexible input for supplies\n */\nexport type Supply = string | HowToSupply | Omit<HowToSupply, \"@type\">;\n\n/**\n * Tool type union - flexible input for tools\n */\nexport type Tool = string | HowToTool | Omit<HowToTool, \"@type\">;\n\n/**\n * EstimatedCost type union - flexible input for cost\n */\nexport type EstimatedCost =\n  | string\n  | SimpleMonetaryAmount\n  | Omit<SimpleMonetaryAmount, \"@type\">;\n\n/**\n * Yield type union - flexible input for yield/result quantity\n */\nexport type HowToYield =\n  | string\n  | QuantitativeValue\n  | Omit<QuantitativeValue, \"@type\">;\n\n/**\n * HowTo - Instructions that explain how to achieve a result\n * @see https://schema.org/HowTo\n */\nexport interface HowTo {\n  \"@type\": \"HowTo\";\n  name: string;\n  description?: string;\n  image?: string | ImageObject | Omit<ImageObject, \"@type\">;\n  estimatedCost?: EstimatedCost;\n  performTime?: string;\n  prepTime?: string;\n  totalTime?: string;\n  step?: Step | Step[];\n  supply?: Supply | Supply[];\n  tool?: Tool | Tool[];\n  yield?: HowToYield;\n  video?: VideoObject | Omit<VideoObject, \"@type\">;\n}\n\n/**\n * Props for the HowToJsonLd component\n */\nexport type HowToJsonLdProps = Omit<HowTo, \"@type\"> & {\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/image.types.ts",
    "content": "import type { Author } from \"./common.types\";\n\n// Base interface for image metadata\nexport interface ImageBase {\n  contentUrl: string;\n  creator?: Author | Author[];\n  creditText?: string;\n  copyrightNotice?: string;\n  license?: string;\n  acquireLicensePage?: string;\n}\n\n// Schema.org ImageObject with metadata extensions\nexport interface ImageMetadata extends ImageBase {\n  \"@type\": \"ImageObject\";\n}\n\n// Component props type - either single image or multiple images\nexport type ImageJsonLdProps = {\n  scriptId?: string;\n  scriptKey?: string;\n} & (\n  | Omit<ImageMetadata, \"@type\">\n  | {\n      images: Omit<ImageMetadata, \"@type\">[];\n    }\n);\n"
  },
  {
    "path": "src/types/index.ts",
    "content": "// Public type exports\nexport * from \"./common.types\";\n// Add other type exports here\n"
  },
  {
    "path": "src/types/jobposting.types.ts",
    "content": "import type {\n  Organization,\n  PostalAddress,\n  QuantitativeValue,\n} from \"./common.types\";\n\n// Administrative area types for applicant location requirements\nexport interface Country {\n  \"@type\": \"Country\";\n  name: string;\n}\n\nexport interface State {\n  \"@type\": \"State\";\n  name: string;\n}\n\nexport type AdministrativeArea = Country | State;\n\n// Place type for job location\nexport interface Place {\n  \"@type\": \"Place\";\n  address?: string | PostalAddress | Omit<PostalAddress, \"@type\">;\n}\n\n// PropertyValue for identifier\nexport interface PropertyValue {\n  \"@type\": \"PropertyValue\";\n  name?: string;\n  value?: string;\n}\n\n// MonetaryAmount for salary\nexport interface MonetaryAmount {\n  \"@type\": \"MonetaryAmount\";\n  currency: string;\n  value: QuantitativeValue | Omit<QuantitativeValue, \"@type\">;\n}\n\n// Education requirements\nexport interface EducationalOccupationalCredential {\n  \"@type\": \"EducationalOccupationalCredential\";\n  credentialCategory?: string;\n}\n\n// Experience requirements\nexport interface OccupationalExperienceRequirements {\n  \"@type\": \"OccupationalExperienceRequirements\";\n  monthsOfExperience?: number;\n}\n\n// Employment type enum values\nexport type EmploymentType =\n  | \"FULL_TIME\"\n  | \"PART_TIME\"\n  | \"CONTRACTOR\"\n  | \"TEMPORARY\"\n  | \"INTERN\"\n  | \"VOLUNTEER\"\n  | \"PER_DIEM\"\n  | \"OTHER\";\n\n// Job location type for remote work\nexport type JobLocationType = \"TELECOMMUTE\";\n\n// Base interface for JobPosting\nexport interface JobPostingBase {\n  title: string;\n  description: string;\n  datePosted: string;\n  hiringOrganization: string | Organization | Omit<Organization, \"@type\">;\n  jobLocation?:\n    | string\n    | Place\n    | Omit<Place, \"@type\">\n    | (string | Place | Omit<Place, \"@type\">)[];\n  url?: string;\n  validThrough?: string;\n  employmentType?: EmploymentType | EmploymentType[];\n  identifier?: string | PropertyValue | Omit<PropertyValue, \"@type\">;\n  baseSalary?: MonetaryAmount | Omit<MonetaryAmount, \"@type\">;\n  applicantLocationRequirements?:\n    | Omit<Country, \"@type\">\n    | Omit<State, \"@type\">\n    | Country\n    | State\n    | (Omit<Country, \"@type\"> | Omit<State, \"@type\"> | Country | State)[];\n  jobLocationType?: JobLocationType;\n  directApply?: boolean;\n  educationRequirements?:\n    | string\n    | EducationalOccupationalCredential\n    | Omit<EducationalOccupationalCredential, \"@type\">\n    | (\n        | string\n        | EducationalOccupationalCredential\n        | Omit<EducationalOccupationalCredential, \"@type\">\n      )[];\n  experienceRequirements?:\n    | string\n    | OccupationalExperienceRequirements\n    | Omit<OccupationalExperienceRequirements, \"@type\">;\n  experienceInPlaceOfEducation?: boolean;\n}\n\n// JobPosting with @type\nexport interface JobPosting extends JobPostingBase {\n  \"@type\": \"JobPosting\";\n}\n\n// Component props type\nexport type JobPostingJsonLdProps = Omit<JobPostingBase, \"@type\"> & {\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/localbusiness.types.ts",
    "content": "import type {\n  ImageObject,\n  PostalAddress,\n  Organization,\n  GeoCoordinates,\n  OpeningHoursSpecification,\n  Review,\n  AggregateRating,\n} from \"./common.types\";\n\nexport interface LocalBusinessBase {\n  name: string;\n  address:\n    | string\n    | PostalAddress\n    | Omit<PostalAddress, \"@type\">\n    | (string | PostalAddress | Omit<PostalAddress, \"@type\">)[];\n  url?: string;\n  telephone?: string;\n  image?:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  priceRange?: string;\n  geo?: GeoCoordinates | Omit<GeoCoordinates, \"@type\">;\n  openingHoursSpecification?:\n    | OpeningHoursSpecification\n    | Omit<OpeningHoursSpecification, \"@type\">\n    | OpeningHoursSpecification[]\n    | Omit<OpeningHoursSpecification, \"@type\">[];\n  review?: Review | Omit<Review, \"@type\"> | Review[] | Omit<Review, \"@type\">[];\n  aggregateRating?: AggregateRating | Omit<AggregateRating, \"@type\">;\n  department?: LocalBusinessBase | LocalBusinessBase[];\n  menu?: string;\n  servesCuisine?: string | string[];\n  sameAs?: string | string[];\n  branchOf?: Organization;\n  currenciesAccepted?: string;\n  paymentAccepted?: string;\n  areaServed?: string | string[];\n  email?: string;\n  faxNumber?: string;\n  slogan?: string;\n  description?: string;\n  publicAccess?: boolean;\n  smokingAllowed?: boolean;\n  isAccessibleForFree?: boolean;\n}\n\nexport interface LocalBusiness extends LocalBusinessBase {\n  \"@type\": \"LocalBusiness\" | string | string[];\n}\n\nexport interface Restaurant extends LocalBusinessBase {\n  \"@type\": \"Restaurant\";\n  servesCuisine?: string | string[];\n  menu?: string;\n}\n\nexport interface Store extends LocalBusinessBase {\n  \"@type\": \"Store\";\n}\n\nexport interface Pharmacy extends LocalBusinessBase {\n  \"@type\": \"Pharmacy\";\n}\n\nexport interface DaySpa extends LocalBusinessBase {\n  \"@type\": \"DaySpa\";\n}\n\nexport interface HealthClub extends LocalBusinessBase {\n  \"@type\": \"HealthClub\";\n}\n\nexport interface EntertainmentBusiness extends LocalBusinessBase {\n  \"@type\": \"EntertainmentBusiness\";\n}\n\nexport interface Electrician extends LocalBusinessBase {\n  \"@type\": \"Electrician\";\n}\n\nexport interface Plumber extends LocalBusinessBase {\n  \"@type\": \"Plumber\";\n}\n\nexport interface Locksmith extends LocalBusinessBase {\n  \"@type\": \"Locksmith\";\n}\n\nexport type LocalBusinessJsonLdProps = Omit<LocalBusinessBase, \"department\"> & {\n  type?: string | string[];\n  scriptId?: string;\n  scriptKey?: string;\n  department?:\n    | (Omit<LocalBusinessBase, \"department\"> & { type?: string | string[] })\n    | (Omit<LocalBusinessBase, \"department\"> & { type?: string | string[] })[];\n};\n"
  },
  {
    "path": "src/types/merchantreturnpolicy.types.ts",
    "content": "import type { MerchantReturnPolicy } from \"./common.types\";\n\n// Component props type that makes @type optional following library pattern\nexport type MerchantReturnPolicyJsonLdProps = Omit<\n  MerchantReturnPolicy,\n  \"@type\"\n> & {\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/movie-carousel.types.ts",
    "content": "import type {\n  ImageObject,\n  Person,\n  Review,\n  AggregateRating,\n} from \"./common.types\";\n\n// Director can be a string or Person\nexport type Director = string | Person | Omit<Person, \"@type\">;\n\n// Movie schema type\nexport interface Movie {\n  \"@type\": \"Movie\";\n  name: string;\n  image: string | ImageObject | (string | ImageObject)[];\n  url?: string;\n  dateCreated?: string;\n  director?: Director;\n  review?: Review | Omit<Review, \"@type\">;\n  aggregateRating?: AggregateRating | Omit<AggregateRating, \"@type\">;\n}\n\n// Flexible input type for movies in the carousel\nexport type MovieListItem = Omit<Movie, \"@type\" | \"image\"> & {\n  image:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n};\n\n// ListItem for ItemList (used in both summary and all-in-one patterns)\nexport interface MovieCarouselListItem {\n  \"@type\": \"ListItem\";\n  position: number;\n  url?: string; // For summary page pattern\n  item?: Movie; // For all-in-one page pattern\n}\n\n// ItemList for movie carousel\nexport interface MovieCarouselItemList {\n  \"@context\": \"https://schema.org\";\n  \"@type\": \"ItemList\";\n  itemListElement: MovieCarouselListItem[];\n}\n\n// Summary page item - just URL with optional position\nexport type SummaryPageItem = string | { url: string; position?: number };\n\n// Component props supporting both patterns\nexport type MovieCarouselJsonLdProps = {\n  scriptId?: string;\n  scriptKey?: string;\n} & (\n  | {\n      // Summary page pattern - just URLs\n      urls: SummaryPageItem[];\n    }\n  | {\n      // All-in-one page pattern - full movie data\n      movies: MovieListItem[];\n    }\n);\n"
  },
  {
    "path": "src/types/organization.types.ts",
    "content": "import type {\n  Organization,\n  MerchantReturnPolicy,\n  MemberProgram,\n} from \"./common.types\";\n\n// OrganizationBase contains all properties except @type\n// This is used by OnlineStore and other organization subtypes\ntype OrganizationBase = Omit<Organization, \"@type\">;\n\nexport interface OnlineStore extends OrganizationBase {\n  \"@type\": \"OnlineStore\";\n  hasMerchantReturnPolicy?:\n    | MerchantReturnPolicy\n    | Omit<MerchantReturnPolicy, \"@type\">\n    | MerchantReturnPolicy[]\n    | Omit<MerchantReturnPolicy, \"@type\">[];\n  hasMemberProgram?:\n    | MemberProgram\n    | Omit<MemberProgram, \"@type\">\n    | MemberProgram[]\n    | Omit<MemberProgram, \"@type\">[];\n}\n\nexport type OrganizationJsonLdProps = (\n  | Omit<Organization, \"@type\">\n  | Omit<OnlineStore, \"@type\">\n) & {\n  type?: \"Organization\" | \"OnlineStore\";\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/product.types.ts",
    "content": "import type {\n  ImageObject,\n  Review,\n  AggregateRating,\n  Brand,\n  Organization,\n  Person,\n  MerchantReturnPolicy,\n  UnitPriceSpecification,\n  OfferShippingDetails,\n  Certification,\n  PeopleAudience,\n  SizeSpecification,\n  ThreeDModel,\n} from \"./common.types\";\n\n// ItemAvailability enum\nexport type ItemAvailability =\n  | \"https://schema.org/BackOrder\"\n  | \"https://schema.org/Discontinued\"\n  | \"https://schema.org/InStock\"\n  | \"https://schema.org/InStoreOnly\"\n  | \"https://schema.org/LimitedAvailability\"\n  | \"https://schema.org/OnlineOnly\"\n  | \"https://schema.org/OutOfStock\"\n  | \"https://schema.org/PreOrder\"\n  | \"https://schema.org/PreSale\"\n  | \"https://schema.org/SoldOut\"\n  | \"BackOrder\"\n  | \"Discontinued\"\n  | \"InStock\"\n  | \"InStoreOnly\"\n  | \"LimitedAvailability\"\n  | \"OnlineOnly\"\n  | \"OutOfStock\"\n  | \"PreOrder\"\n  | \"PreSale\"\n  | \"SoldOut\";\n\n// PriceSpecification for complex pricing\nexport interface PriceSpecification {\n  \"@type\": \"PriceSpecification\";\n  price: number | string;\n  priceCurrency?: string;\n  minPrice?: number;\n  maxPrice?: number;\n}\n\n// Product Offer type - extends the basic event Offer\nexport interface ProductOffer {\n  \"@type\": \"Offer\";\n  url?: string;\n  price?: number | string;\n  priceCurrency?: string;\n  priceSpecification?:\n    | PriceSpecification\n    | UnitPriceSpecification\n    | Omit<PriceSpecification, \"@type\">\n    | Omit<UnitPriceSpecification, \"@type\">\n    | (\n        | PriceSpecification\n        | UnitPriceSpecification\n        | Omit<PriceSpecification, \"@type\">\n        | Omit<UnitPriceSpecification, \"@type\">\n      )[];\n  availability?: ItemAvailability;\n  availabilityStarts?: string;\n  availabilityEnds?: string;\n  priceValidUntil?: string;\n  itemCondition?:\n    | \"https://schema.org/NewCondition\"\n    | \"https://schema.org/UsedCondition\"\n    | \"https://schema.org/DamagedCondition\"\n    | \"https://schema.org/RefurbishedCondition\"\n    | \"NewCondition\"\n    | \"UsedCondition\"\n    | \"DamagedCondition\"\n    | \"RefurbishedCondition\";\n  seller?:\n    | string\n    | Organization\n    | Person\n    | Omit<Organization, \"@type\">\n    | Omit<Person, \"@type\">;\n  shippingDetails?:\n    | OfferShippingDetails\n    | Omit<OfferShippingDetails, \"@type\">\n    | (OfferShippingDetails | Omit<OfferShippingDetails, \"@type\">)[];\n  hasMerchantReturnPolicy?:\n    | MerchantReturnPolicy\n    | Omit<MerchantReturnPolicy, \"@type\">\n    | MerchantReturnPolicy[]\n    | Omit<MerchantReturnPolicy, \"@type\">[];\n}\n\n// AggregateOffer for multiple sellers\nexport interface AggregateOffer {\n  \"@type\": \"AggregateOffer\";\n  lowPrice: number | string;\n  priceCurrency: string;\n  highPrice?: number | string;\n  offerCount?: number;\n  offers?: ProductOffer[] | Omit<ProductOffer, \"@type\">[];\n}\n\n// ListItem for pros and cons\nexport interface ProductListItem {\n  \"@type\": \"ListItem\";\n  position?: number;\n  name: string;\n}\n\n// ItemList for pros and cons\nexport interface ProductItemList {\n  \"@type\": \"ItemList\";\n  itemListElement: (ProductListItem | Omit<ProductListItem, \"@type\">)[];\n}\n\n// Extended Review with pros/cons\nexport interface ProductReview extends Omit<Review, \"@type\"> {\n  \"@type\": \"Review\";\n  name?: string;\n  positiveNotes?: ProductItemList | Omit<ProductItemList, \"@type\">;\n  negativeNotes?: ProductItemList | Omit<ProductItemList, \"@type\">;\n}\n\n// Product base interface\nexport interface ProductBase {\n  name: string;\n  description?: string;\n  image?:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  sku?: string;\n  mpn?: string;\n  gtin?: string;\n  gtin8?: string;\n  gtin12?: string;\n  gtin13?: string;\n  gtin14?: string;\n  brand?: string | Brand | Omit<Brand, \"@type\">;\n  review?:\n    | ProductReview\n    | Omit<ProductReview, \"@type\">\n    | (ProductReview | Omit<ProductReview, \"@type\">)[];\n  aggregateRating?: AggregateRating | Omit<AggregateRating, \"@type\">;\n  offers?:\n    | ProductOffer\n    | AggregateOffer\n    | Omit<ProductOffer, \"@type\">\n    | Omit<AggregateOffer, \"@type\">\n    | (ProductOffer | Omit<ProductOffer, \"@type\">)[];\n  category?: string;\n  color?: string;\n  material?: string;\n  model?: string;\n  productID?: string;\n  url?: string;\n  weight?:\n    | string\n    | {\n        \"@type\"?: \"QuantitativeValue\";\n        value?: number;\n        unitCode?: string;\n        unitText?: string;\n      };\n  width?:\n    | string\n    | {\n        \"@type\"?: \"QuantitativeValue\";\n        value?: number;\n        unitCode?: string;\n        unitText?: string;\n      };\n  height?:\n    | string\n    | {\n        \"@type\"?: \"QuantitativeValue\";\n        value?: number;\n        unitCode?: string;\n        unitText?: string;\n      };\n  depth?:\n    | string\n    | {\n        \"@type\"?: \"QuantitativeValue\";\n        value?: number;\n        unitCode?: string;\n        unitText?: string;\n      };\n  additionalProperty?: {\n    \"@type\": \"PropertyValue\";\n    name: string;\n    value: string | number;\n  }[];\n  manufacturer?:\n    | string\n    | Organization\n    | Person\n    | Omit<Organization, \"@type\">\n    | Omit<Person, \"@type\">;\n  releaseDate?: string;\n  productionDate?: string;\n  purchaseDate?: string;\n  expirationDate?: string;\n  award?: string | string[];\n  // Additional properties for products\n  hasCertification?:\n    | Certification\n    | Omit<Certification, \"@type\">\n    | (Certification | Omit<Certification, \"@type\">)[];\n  audience?: PeopleAudience | Omit<PeopleAudience, \"@type\">;\n  size?: string | SizeSpecification | Omit<SizeSpecification, \"@type\">;\n  pattern?: string;\n  isbn?: string;\n  subjectOf?: ThreeDModel | Omit<ThreeDModel, \"@type\">;\n  // Properties for variant relationships\n  isVariantOf?: { \"@id\": string } | ProductGroup | Omit<ProductGroup, \"@type\">;\n  inProductGroupWithID?: string;\n}\n\n// Product type\nexport interface Product extends ProductBase {\n  \"@type\": \"Product\" | [\"Product\", \"Car\"];\n}\n\n// VariesBy type for specifying variant properties\nexport type VariesBy =\n  | \"https://schema.org/color\"\n  | \"https://schema.org/size\"\n  | \"https://schema.org/material\"\n  | \"https://schema.org/pattern\"\n  | \"https://schema.org/suggestedAge\"\n  | \"https://schema.org/suggestedGender\"\n  | \"color\"\n  | \"size\"\n  | \"material\"\n  | \"pattern\"\n  | \"suggestedAge\"\n  | \"suggestedGender\";\n\n// ProductGroup interface for grouping product variants\nexport interface ProductGroup {\n  \"@type\": \"ProductGroup\";\n  \"@id\"?: string;\n  name: string;\n  description?: string;\n  url?: string;\n  image?:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  brand?:\n    | string\n    | Brand\n    | Organization\n    | Omit<Brand, \"@type\">\n    | Omit<Organization, \"@type\">;\n  review?:\n    | ProductReview\n    | Omit<ProductReview, \"@type\">\n    | (ProductReview | Omit<ProductReview, \"@type\">)[];\n  aggregateRating?: AggregateRating | Omit<AggregateRating, \"@type\">;\n  audience?: {\n    \"@type\"?: \"PeopleAudience\";\n    suggestedGender?: \"male\" | \"female\" | \"unisex\";\n    suggestedAge?: {\n      \"@type\"?: \"QuantitativeValue\";\n      minValue?: number;\n      maxValue?: number;\n      unitCode?: string;\n    };\n  };\n  productGroupID: string;\n  variesBy?: VariesBy | VariesBy[];\n  hasVariant?: (\n    | Product\n    | Omit<Product, \"@type\">\n    | { url: string }\n    | { \"@type\": \"Product\"; url: string }\n  )[];\n  pattern?: string;\n  material?: string;\n  category?: string;\n}\n\n// Component props\nexport type ProductJsonLdProps = (\n  | (Omit<Product, \"@type\"> & {\n      type?: \"Product\";\n      isCar?: boolean; // Helper prop to add Car type alongside Product\n    })\n  | (Omit<ProductGroup, \"@type\"> & {\n      type?: \"ProductGroup\";\n    })\n) & {\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/profile.types.ts",
    "content": "import type { Person, Organization } from \"./common.types\";\n\n// ProfilePage structured data type\nexport interface ProfilePage {\n  \"@type\": \"ProfilePage\";\n  \"@context\": \"https://schema.org\";\n  mainEntity: Person | Organization;\n  dateCreated?: string;\n  dateModified?: string;\n  hasPart?: unknown[]; // For including recent activity\n}\n\n// Props for the component - mainEntity can be a string, object without @type, or with @type\nexport type ProfilePageJsonLdProps = {\n  mainEntity:\n    | string\n    | Person\n    | Organization\n    | Omit<Person, \"@type\">\n    | Omit<Organization, \"@type\">;\n  dateCreated?: string;\n  dateModified?: string;\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/quiz.types.ts",
    "content": "// Type definitions for Education Q&A (Quiz) structured data based on Schema.org\n\nimport type { Thing } from \"./common.types\";\n\n// Answer type according to Schema.org\nexport interface Answer {\n  \"@type\": \"Answer\";\n  text: string;\n}\n\n// Question type for Quiz (Education Q&A)\nexport interface Question {\n  \"@context\"?: \"https://schema.org/\";\n  \"@type\": \"Question\";\n  eduQuestionType: \"Flashcard\";\n  text: string;\n  acceptedAnswer: Answer;\n}\n\n// AlignmentObject for educational alignment\nexport interface AlignmentObject {\n  \"@type\": \"AlignmentObject\";\n  alignmentType: \"educationalSubject\" | \"educationalLevel\";\n  targetName: string;\n}\n\n// Quiz type according to Schema.org\nexport interface Quiz {\n  \"@context\": \"https://schema.org/\";\n  \"@type\": \"Quiz\";\n  about?: Thing;\n  educationalAlignment?: AlignmentObject[];\n  hasPart: Question[];\n}\n\n// Flexible input type for questions - support both simple and complex formats\nexport type QuestionInput =\n  | string // Just the question text with answer\n  | {\n      question: string;\n      answer: string;\n    }\n  | {\n      text: string;\n      acceptedAnswer: string | Answer;\n    }\n  | Omit<Question, \"@type\" | \"@context\" | \"eduQuestionType\">;\n\n// Component props\nexport interface QuizJsonLdProps {\n  questions: QuestionInput[];\n  about?: string | Thing;\n  educationalAlignment?: Array<{\n    type: \"educationalSubject\" | \"educationalLevel\";\n    name: string;\n  }>;\n  scriptId?: string;\n  scriptKey?: string;\n}\n"
  },
  {
    "path": "src/types/recipe.types.ts",
    "content": "import type {\n  ImageObject,\n  Author,\n  AggregateRating,\n  VideoObject,\n} from \"./common.types\";\n\nexport interface NutritionInformation {\n  \"@type\": \"NutritionInformation\";\n  calories?: string;\n  carbohydrateContent?: string;\n  proteinContent?: string;\n  fatContent?: string;\n  saturatedFatContent?: string;\n  unsaturatedFatContent?: string;\n  transFatContent?: string;\n  cholesterolContent?: string;\n  sodiumContent?: string;\n  fiberContent?: string;\n  sugarContent?: string;\n  servingSize?: string;\n}\n\nexport interface HowToStep {\n  \"@type\": \"HowToStep\";\n  name?: string;\n  text: string;\n  url?: string;\n  image?: string | ImageObject | Omit<ImageObject, \"@type\">;\n}\n\nexport interface HowToSection {\n  \"@type\": \"HowToSection\";\n  name: string;\n  itemListElement: (HowToStep | Omit<HowToStep, \"@type\">)[];\n}\n\nexport type Instruction =\n  | string\n  | HowToStep\n  | HowToSection\n  | Omit<HowToStep, \"@type\">\n  | Omit<HowToSection, \"@type\">;\nexport type RecipeImage =\n  | string\n  | ImageObject\n  | Omit<ImageObject, \"@type\">\n  | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n\nexport interface Recipe {\n  \"@type\": \"Recipe\";\n  name: string;\n  image: RecipeImage;\n  description?: string;\n  author?: Author;\n  datePublished?: string;\n  prepTime?: string;\n  cookTime?: string;\n  totalTime?: string;\n  recipeYield?: string | number;\n  recipeCategory?: string;\n  recipeCuisine?: string;\n  nutrition?: Omit<NutritionInformation, \"@type\"> & {\n    \"@type\"?: \"NutritionInformation\";\n  };\n  recipeIngredient?: string[];\n  recipeInstructions?: Instruction | Instruction[];\n  aggregateRating?: AggregateRating | Omit<AggregateRating, \"@type\">;\n  video?: VideoObject | Omit<VideoObject, \"@type\">;\n  keywords?: string;\n  url?: string;\n}\n\nexport type RecipeJsonLdProps = Omit<Recipe, \"@type\"> & {\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/review.types.ts",
    "content": "import type { Author } from \"~/types/common.types\";\n\nexport type ItemReviewedType =\n  | \"Book\"\n  | \"Course\"\n  | \"CreativeWorkSeason\"\n  | \"CreativeWorkSeries\"\n  | \"Episode\"\n  | \"Event\"\n  | \"Game\"\n  | \"HowTo\"\n  | \"LocalBusiness\"\n  | \"MediaObject\"\n  | \"Movie\"\n  | \"MusicPlaylist\"\n  | \"MusicRecording\"\n  | \"Organization\"\n  | \"Product\"\n  | \"Recipe\"\n  | \"SoftwareApplication\";\n\nexport type ItemReviewedInput =\n  | string\n  | ({ name: string; \"@type\"?: ItemReviewedType } & Record<string, unknown>);\n\nexport interface ReviewJsonLdProps {\n  // JSON-LD plumbing\n  scriptId?: string;\n  scriptKey?: string;\n\n  // Required per Google Review\n  author: Author;\n  reviewRating:\n    | {\n        \"@type\"?: \"Rating\";\n        ratingValue: number | string;\n        bestRating?: number | string;\n        worstRating?: number | string;\n      }\n    | {\n        \"@type\": \"AggregateRating\";\n        ratingValue: number | string;\n        bestRating?: number | string;\n        worstRating?: number | string;\n      };\n  itemReviewed: ItemReviewedInput;\n\n  // Recommended/optional\n  datePublished?: string;\n  reviewBody?: string;\n  publisher?: Author;\n  url?: string;\n  mainEntityOfPage?: string | { \"@type\"?: \"WebPage\"; \"@id\": string };\n}\n\nexport interface AggregateRatingJsonLdProps {\n  scriptId?: string;\n  scriptKey?: string;\n\n  itemReviewed: ItemReviewedInput;\n  ratingValue: number | string;\n  ratingCount?: number;\n  reviewCount?: number;\n  bestRating?: number;\n  worstRating?: number;\n}\n"
  },
  {
    "path": "src/types/softwareApplication.types.ts",
    "content": "import type {\n  ImageObject,\n  Organization,\n  Author,\n  AggregateRating,\n  Review,\n} from \"./common.types\";\nimport type { Offer } from \"./event.types\";\n\n// Base interface with common properties for all software applications\nexport interface SoftwareApplicationBase {\n  name?: string;\n  description?: string;\n  url?: string;\n  image?:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  applicationCategory?: string;\n  applicationSubCategory?: string;\n  applicationSuite?: string;\n  operatingSystem?: string;\n  memoryRequirements?: string;\n  processorRequirements?: string;\n  storageRequirements?: string;\n  availableOnDevice?: string;\n  downloadUrl?: string;\n  installUrl?: string;\n  countriesSupported?: string | string[];\n  countriesNotSupported?: string | string[];\n  permissions?: string | string[];\n  softwareVersion?: string;\n  releaseNotes?: string;\n  screenshot?:\n    | string\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  featureList?: string | string[];\n  offers?: Offer | Omit<Offer, \"@type\"> | (Offer | Omit<Offer, \"@type\">)[];\n  aggregateRating?: AggregateRating | Omit<AggregateRating, \"@type\">;\n  review?: Review | Omit<Review, \"@type\"> | (Review | Omit<Review, \"@type\">)[];\n  author?: Author;\n  publisher?: Organization | Omit<Organization, \"@type\">;\n  datePublished?: string;\n  dateModified?: string;\n  contentRating?: string;\n}\n\n// Specific schema type interfaces\nexport interface SoftwareApplication extends SoftwareApplicationBase {\n  \"@type\": \"SoftwareApplication\";\n}\n\nexport interface MobileApplication extends SoftwareApplicationBase {\n  \"@type\": \"MobileApplication\";\n}\n\nexport interface WebApplication extends SoftwareApplicationBase {\n  \"@type\": \"WebApplication\";\n}\n\nexport interface VideoGame extends SoftwareApplicationBase {\n  \"@type\": \"VideoGame\";\n}\n\n// Specific application category types\nexport interface GameApplication extends SoftwareApplicationBase {\n  \"@type\": \"GameApplication\";\n}\n\nexport interface SocialNetworkingApplication extends SoftwareApplicationBase {\n  \"@type\": \"SocialNetworkingApplication\";\n}\n\nexport interface TravelApplication extends SoftwareApplicationBase {\n  \"@type\": \"TravelApplication\";\n}\n\nexport interface ShoppingApplication extends SoftwareApplicationBase {\n  \"@type\": \"ShoppingApplication\";\n}\n\nexport interface SportsApplication extends SoftwareApplicationBase {\n  \"@type\": \"SportsApplication\";\n}\n\nexport interface LifestyleApplication extends SoftwareApplicationBase {\n  \"@type\": \"LifestyleApplication\";\n}\n\nexport interface BusinessApplication extends SoftwareApplicationBase {\n  \"@type\": \"BusinessApplication\";\n}\n\nexport interface DesignApplication extends SoftwareApplicationBase {\n  \"@type\": \"DesignApplication\";\n}\n\nexport interface DeveloperApplication extends SoftwareApplicationBase {\n  \"@type\": \"DeveloperApplication\";\n}\n\nexport interface DriverApplication extends SoftwareApplicationBase {\n  \"@type\": \"DriverApplication\";\n}\n\nexport interface EducationalApplication extends SoftwareApplicationBase {\n  \"@type\": \"EducationalApplication\";\n}\n\nexport interface HealthApplication extends SoftwareApplicationBase {\n  \"@type\": \"HealthApplication\";\n}\n\nexport interface FinanceApplication extends SoftwareApplicationBase {\n  \"@type\": \"FinanceApplication\";\n}\n\nexport interface SecurityApplication extends SoftwareApplicationBase {\n  \"@type\": \"SecurityApplication\";\n}\n\nexport interface BrowserApplication extends SoftwareApplicationBase {\n  \"@type\": \"BrowserApplication\";\n}\n\nexport interface CommunicationApplication extends SoftwareApplicationBase {\n  \"@type\": \"CommunicationApplication\";\n}\n\nexport interface DesktopEnhancementApplication extends SoftwareApplicationBase {\n  \"@type\": \"DesktopEnhancementApplication\";\n}\n\nexport interface EntertainmentApplication extends SoftwareApplicationBase {\n  \"@type\": \"EntertainmentApplication\";\n}\n\nexport interface MultimediaApplication extends SoftwareApplicationBase {\n  \"@type\": \"MultimediaApplication\";\n}\n\nexport interface HomeApplication extends SoftwareApplicationBase {\n  \"@type\": \"HomeApplication\";\n}\n\nexport interface UtilitiesApplication extends SoftwareApplicationBase {\n  \"@type\": \"UtilitiesApplication\";\n}\n\nexport interface ReferenceApplication extends SoftwareApplicationBase {\n  \"@type\": \"ReferenceApplication\";\n}\n\n// Type for VideoGame co-typed with another application type\nexport type VideoGameCoTyped =\n  | [\"VideoGame\", \"MobileApplication\"]\n  | [\"VideoGame\", \"WebApplication\"]\n  | [\"VideoGame\", \"SoftwareApplication\"];\n\n// All possible application types\nexport type ApplicationType =\n  | \"SoftwareApplication\"\n  | \"MobileApplication\"\n  | \"WebApplication\"\n  | \"GameApplication\"\n  | \"SocialNetworkingApplication\"\n  | \"TravelApplication\"\n  | \"ShoppingApplication\"\n  | \"SportsApplication\"\n  | \"LifestyleApplication\"\n  | \"BusinessApplication\"\n  | \"DesignApplication\"\n  | \"DeveloperApplication\"\n  | \"DriverApplication\"\n  | \"EducationalApplication\"\n  | \"HealthApplication\"\n  | \"FinanceApplication\"\n  | \"SecurityApplication\"\n  | \"BrowserApplication\"\n  | \"CommunicationApplication\"\n  | \"DesktopEnhancementApplication\"\n  | \"EntertainmentApplication\"\n  | \"MultimediaApplication\"\n  | \"HomeApplication\"\n  | \"UtilitiesApplication\"\n  | \"ReferenceApplication\";\n\n// Component props type\nexport type SoftwareApplicationJsonLdProps = (\n  | Omit<SoftwareApplication, \"@type\">\n  | Omit<MobileApplication, \"@type\">\n  | Omit<WebApplication, \"@type\">\n  | Omit<GameApplication, \"@type\">\n  | Omit<SocialNetworkingApplication, \"@type\">\n  | Omit<TravelApplication, \"@type\">\n  | Omit<ShoppingApplication, \"@type\">\n  | Omit<SportsApplication, \"@type\">\n  | Omit<LifestyleApplication, \"@type\">\n  | Omit<BusinessApplication, \"@type\">\n  | Omit<DesignApplication, \"@type\">\n  | Omit<DeveloperApplication, \"@type\">\n  | Omit<DriverApplication, \"@type\">\n  | Omit<EducationalApplication, \"@type\">\n  | Omit<HealthApplication, \"@type\">\n  | Omit<FinanceApplication, \"@type\">\n  | Omit<SecurityApplication, \"@type\">\n  | Omit<BrowserApplication, \"@type\">\n  | Omit<CommunicationApplication, \"@type\">\n  | Omit<DesktopEnhancementApplication, \"@type\">\n  | Omit<EntertainmentApplication, \"@type\">\n  | Omit<MultimediaApplication, \"@type\">\n  | Omit<HomeApplication, \"@type\">\n  | Omit<UtilitiesApplication, \"@type\">\n  | Omit<ReferenceApplication, \"@type\">\n) & {\n  type?: ApplicationType | VideoGameCoTyped;\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/vacationrental.types.ts",
    "content": "import type {\n  ImageObject,\n  PostalAddress,\n  Review,\n  AggregateRating,\n  Brand,\n  Accommodation,\n} from \"./common.types\";\n\nexport interface VacationRentalBase {\n  // Required properties\n  containsPlace: Accommodation | Omit<Accommodation, \"@type\">;\n  identifier: string;\n  image:\n    | string\n    | string[]\n    | ImageObject\n    | Omit<ImageObject, \"@type\">\n    | (string | ImageObject | Omit<ImageObject, \"@type\">)[];\n  latitude: number | string;\n  longitude: number | string;\n  name: string;\n\n  // Recommended properties\n  additionalType?: string;\n  address?: PostalAddress | Omit<PostalAddress, \"@type\">;\n  aggregateRating?: AggregateRating | Omit<AggregateRating, \"@type\">;\n  brand?: Brand | Omit<Brand, \"@type\">;\n  checkinTime?: string;\n  checkoutTime?: string;\n  description?: string;\n  knowsLanguage?: string | string[];\n  review?: Review | Omit<Review, \"@type\"> | (Review | Omit<Review, \"@type\">)[];\n\n  // Alternative location specification\n  geo?: {\n    latitude: number | string;\n    longitude: number | string;\n  };\n}\n\nexport interface VacationRental extends VacationRentalBase {\n  \"@type\": \"VacationRental\";\n}\n\nexport type VacationRentalJsonLdProps = Omit<VacationRentalBase, \"@type\"> & {\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/types/video.types.ts",
    "content": "import type {\n  ImageObject,\n  InteractionCounter,\n  Organization,\n  Author,\n} from \"./common.types\";\n\n// Type for ISO 8601 duration format (e.g., \"PT30M\" for 30 minutes)\nexport type Duration = string;\n\n// Type for thumbnail URLs - can be string, ImageObject, or array\nexport type Thumbnail = string | ImageObject | Omit<ImageObject, \"@type\">;\n\n// Type for regions (ISO 3166-1 country codes)\nexport type Region = string | string[];\n\n// BroadcastEvent for live videos\nexport interface BroadcastEvent {\n  \"@type\": \"BroadcastEvent\";\n  name?: string;\n  isLiveBroadcast: boolean;\n  startDate: string;\n  endDate?: string;\n}\n\n// Clip for video segments/key moments\nexport interface Clip {\n  \"@type\": \"Clip\";\n  name: string;\n  startOffset: number;\n  endOffset?: number;\n  url: string;\n}\n\n// SeekToAction for automatic key moments\nexport interface SeekToAction {\n  \"@type\": \"SeekToAction\";\n  target: string;\n  \"startOffset-input\": string;\n}\n\n// EntryPoint wrapper for SeekToAction\nexport interface PotentialAction {\n  \"@type\": \"SeekToAction\";\n  target: string;\n  \"startOffset-input\": string;\n}\n\n// Enhanced VideoObject with all Google-specified properties\nexport interface VideoObjectBase {\n  name: string;\n  description: string;\n  thumbnailUrl: Thumbnail | Thumbnail[];\n  uploadDate: string;\n  contentUrl?: string;\n  embedUrl?: string;\n  duration?: Duration;\n  expires?: string;\n  interactionStatistic?:\n    | InteractionCounter\n    | Omit<InteractionCounter, \"@type\">\n    | (InteractionCounter | Omit<InteractionCounter, \"@type\">)[];\n  regionsAllowed?: Region;\n  ineligibleRegion?: Region;\n  publication?:\n    | BroadcastEvent\n    | Omit<BroadcastEvent, \"@type\">\n    | (BroadcastEvent | Omit<BroadcastEvent, \"@type\">)[];\n  hasPart?: Clip | Omit<Clip, \"@type\"> | (Clip | Omit<Clip, \"@type\">)[];\n  potentialAction?: PotentialAction | Omit<PotentialAction, \"@type\">;\n  author?: Author | Author[];\n  publisher?: Organization | Omit<Organization, \"@type\">;\n}\n\n// VideoObject schema type\nexport interface VideoObject extends VideoObjectBase {\n  \"@type\": \"VideoObject\";\n}\n\n// Component props - developers don't need to specify @type\nexport type VideoJsonLdProps = Omit<VideoObject, \"@type\"> & {\n  type?: \"VideoObject\";\n  scriptId?: string;\n  scriptKey?: string;\n};\n"
  },
  {
    "path": "src/utils/processors.export.ts",
    "content": "/**\n * Public API for custom component creation\n * These processors help maintain the @type optional pattern\n * and provide flexible input handling for structured data\n */\n\n// Core utility for generic schema type processing\nexport { processSchemaType } from \"./processors\";\n\n// People & Organizations\nexport {\n  processAuthor,\n  processPublisher,\n  processOrganization,\n  processOrganizer,\n  processPerformer,\n  processDirector,\n  processCreator,\n  processFunder,\n  processProvider,\n  processHiringOrganization,\n} from \"./processors\";\n\n// Media & Content\nexport {\n  processImage,\n  processVideo,\n  processLogo,\n  processScreenshot,\n  processClip,\n  processBroadcastEvent,\n  processSeekToAction,\n  processThreeDModel,\n} from \"./processors\";\n\n// Locations & Places\nexport {\n  processAddress,\n  processPlace,\n  processGeo,\n  processJobLocation,\n  processSpatialCoverage,\n  processApplicantLocationRequirements,\n  processDefinedRegion,\n} from \"./processors\";\n\n// Commerce & Offers\nexport {\n  processOffer,\n  processProductOffer,\n  processAggregateOffer,\n  processMerchantReturnPolicy,\n  processReturnPolicySeasonalOverride,\n  processPriceSpecification,\n  processUnitPriceSpecification,\n  processSimpleMonetaryAmount,\n  processMonetaryAmount,\n  processOfferShippingDetails,\n  processShippingDeliveryTime,\n} from \"./processors\";\n\n// Reviews & Ratings\nexport {\n  processReview,\n  processProductReview,\n  processAggregateRating,\n  processRating,\n  processClaimReviewRating,\n  processItemReviewed,\n} from \"./processors\";\n\n// Structured Content\nexport {\n  processInstruction,\n  processNutrition,\n  processBreadcrumbItem,\n  processComment,\n  processWebPageElement,\n  processCertification,\n} from \"./processors\";\n\n// HowTo Content\nexport {\n  processStep,\n  processHowToStep,\n  processHowToSection,\n  processHowToSupply,\n  processHowToTool,\n  processHowToDirection,\n  processHowToTip,\n  processEstimatedCost,\n  processHowToYield,\n} from \"./processors\";\n\n// Membership & Loyalty\nexport {\n  processMemberProgram,\n  processMemberProgramTier,\n  processTierRequirement,\n  processTierBenefit,\n  processMembershipPointsEarned,\n} from \"./processors\";\n\n// Specifications & Values\nexport {\n  processQuantitativeValue,\n  processNumberOfEmployees,\n  processContactPoint,\n  processOpeningHours,\n  processJobPropertyValue,\n  processIdentifier,\n  processPeopleAudience,\n  processSizeSpecification,\n} from \"./processors\";\n\n// Products\nexport {\n  processProductVariant,\n  processVariesBy,\n  processBrand,\n  processProductItemList,\n} from \"./processors\";\n\n// Education & Requirements\nexport {\n  processEducationRequirements,\n  processExperienceRequirements,\n} from \"./processors\";\n\n// Data & Creative Works\nexport {\n  processLicense,\n  processDataDownload,\n  processDataCatalog,\n  processIsPartOf,\n  processMainEntityOfPage,\n  processAppearance,\n  processSharedContent,\n  processClaim,\n} from \"./processors\";\n\n// Accommodation & Rental\nexport {\n  processAccommodation,\n  processBedDetails,\n  processLocationFeatureSpecification,\n} from \"./processors\";\n\n// Interaction & Statistics\nexport { processInteractionStatistic, processFeatureList } from \"./processors\";\n\n// Re-export types that users might need\nexport type { ItemReviewedType } from \"./processors\";\n"
  },
  {
    "path": "src/utils/processors.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport {\n  processImage,\n  processAggregateRating,\n  processLogo,\n  processAuthor,\n  processAddress,\n  processPerformer,\n  processContactPoint,\n  processOrganizer,\n  processReview,\n  processMainEntityOfPage,\n  processMerchantReturnPolicy,\n  processVideo,\n  processInstruction,\n  processDataCatalog,\n  processDataDownload,\n  processLicense,\n} from \"./processors\";\n\ndescribe(\"processImage\", () => {\n  it(\"should return string URL as-is\", () => {\n    const result = processImage(\"https://example.com/image.jpg\");\n    expect(result).toBe(\"https://example.com/image.jpg\");\n  });\n\n  it(\"should return ImageObject with @type as-is\", () => {\n    const imageObject = {\n      \"@type\": \"ImageObject\" as const,\n      url: \"https://example.com/image.jpg\",\n      width: 800,\n      height: 600,\n    };\n    const result = processImage(imageObject);\n    expect(result).toEqual(imageObject);\n  });\n\n  it(\"should add @type to ImageObject without it\", () => {\n    const imageObjectWithoutType = {\n      url: \"https://example.com/image.jpg\",\n      width: 800,\n      height: 600,\n    };\n    const result = processImage(imageObjectWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/image.jpg\",\n      width: 800,\n      height: 600,\n    });\n  });\n});\n\ndescribe(\"processLogo\", () => {\n  it(\"should process logo same as image\", () => {\n    const logoWithoutType = {\n      url: \"https://example.com/logo.png\",\n      width: 200,\n      height: 200,\n    };\n    const result = processLogo(logoWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/logo.png\",\n      width: 200,\n      height: 200,\n    });\n  });\n});\n\ndescribe(\"processAggregateRating\", () => {\n  it(\"should return AggregateRating with @type as-is\", () => {\n    const rating = {\n      \"@type\": \"AggregateRating\" as const,\n      ratingValue: 4.5,\n      ratingCount: 10,\n      bestRating: 5,\n      worstRating: 1,\n    };\n    const result = processAggregateRating(rating);\n    expect(result).toEqual(rating);\n  });\n\n  it(\"should add @type to AggregateRating without it\", () => {\n    const ratingWithoutType = {\n      ratingValue: 4.5,\n      ratingCount: 10,\n      bestRating: 5,\n      worstRating: 1,\n    };\n    const result = processAggregateRating(ratingWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.5,\n      ratingCount: 10,\n      bestRating: 5,\n      worstRating: 1,\n    });\n  });\n});\n\ndescribe(\"processAuthor\", () => {\n  it(\"should convert string to Person\", () => {\n    const result = processAuthor(\"John Doe\");\n    expect(result).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n    });\n  });\n\n  it(\"should return Person with @type as-is\", () => {\n    const person = {\n      \"@type\": \"Person\" as const,\n      name: \"Jane Smith\",\n      givenName: \"Jane\",\n      familyName: \"Smith\",\n    };\n    const result = processAuthor(person);\n    expect(result).toEqual(person);\n  });\n\n  it(\"should return Organization with @type as-is\", () => {\n    const org = {\n      \"@type\": \"Organization\" as const,\n      name: \"Acme Corp\",\n      url: \"https://example.com\",\n      logo: \"https://example.com/logo.png\",\n    };\n    const result = processAuthor(org);\n    expect(result).toEqual(org);\n  });\n\n  it(\"should add @type Person to object without Organization properties\", () => {\n    const personWithoutType = {\n      name: \"John Doe\",\n      givenName: \"John\",\n      familyName: \"Doe\",\n    };\n    const result = processAuthor(personWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Doe\",\n      givenName: \"John\",\n      familyName: \"Doe\",\n    });\n  });\n\n  it(\"should add @type Organization to object with logo\", () => {\n    const orgWithoutType = {\n      name: \"WebDev Solutions\",\n      url: \"https://example.com\",\n      logo: \"https://example.com/logo.png\",\n    };\n    const result = processAuthor(orgWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"Organization\",\n      name: \"WebDev Solutions\",\n      url: \"https://example.com\",\n      logo: \"https://example.com/logo.png\",\n    });\n  });\n\n  it(\"should add @type Organization to object with address\", () => {\n    const orgWithoutType = {\n      name: \"Company Inc\",\n      address: \"123 Main St\",\n    };\n    const result = processAuthor(orgWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Company Inc\",\n      address: \"123 Main St\",\n    });\n  });\n\n  it(\"should add @type Person to object with sameAs (since both Person and Organization can have sameAs)\", () => {\n    const personWithoutType = {\n      name: \"Social Media User\",\n      sameAs: [\"https://twitter.com/user\", \"https://facebook.com/user\"],\n    };\n    const result = processAuthor(personWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"Person\",\n      name: \"Social Media User\",\n      sameAs: [\"https://twitter.com/user\", \"https://facebook.com/user\"],\n    });\n  });\n\n  it(\"should default to Person for ambiguous object with just name\", () => {\n    const ambiguousWithoutType = {\n      name: \"Could be either\",\n    };\n    const result = processAuthor(ambiguousWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"Person\",\n      name: \"Could be either\",\n    });\n  });\n\n  it(\"should add @type Person when object has url but also person properties\", () => {\n    const personWithUrl = {\n      name: \"John Blogger\",\n      url: \"https://johnblog.com\",\n      givenName: \"John\",\n    };\n    const result = processAuthor(personWithUrl);\n    expect(result).toEqual({\n      \"@type\": \"Person\",\n      name: \"John Blogger\",\n      url: \"https://johnblog.com\",\n      givenName: \"John\",\n    });\n  });\n\n  it(\"should process contactPoint in Organization\", () => {\n    const orgWithContactPoint = {\n      name: \"Tech Corp\",\n      logo: \"https://example.com/logo.png\",\n      contactPoint: {\n        contactType: \"customer service\",\n        telephone: \"+1-555-1234\",\n        email: \"support@techcorp.com\",\n      },\n    };\n    const result = processAuthor(orgWithContactPoint);\n    expect(result).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Tech Corp\",\n      logo: \"https://example.com/logo.png\",\n      contactPoint: {\n        \"@type\": \"ContactPoint\",\n        contactType: \"customer service\",\n        telephone: \"+1-555-1234\",\n        email: \"support@techcorp.com\",\n      },\n    });\n  });\n\n  it(\"should process array of contactPoints in Organization\", () => {\n    const orgWithMultipleContactPoints = {\n      name: \"Global Corp\",\n      logo: \"https://example.com/logo.png\",\n      contactPoint: [\n        {\n          contactType: \"customer service\",\n          telephone: \"+1-555-1234\",\n        },\n        {\n          contactType: \"technical support\",\n          telephone: \"+1-555-5678\",\n          email: \"tech@globalcorp.com\",\n        },\n      ],\n    };\n    const result = processAuthor(orgWithMultipleContactPoints);\n    expect(result).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Global Corp\",\n      logo: \"https://example.com/logo.png\",\n      contactPoint: [\n        {\n          \"@type\": \"ContactPoint\",\n          contactType: \"customer service\",\n          telephone: \"+1-555-1234\",\n        },\n        {\n          \"@type\": \"ContactPoint\",\n          contactType: \"technical support\",\n          telephone: \"+1-555-5678\",\n          email: \"tech@globalcorp.com\",\n        },\n      ],\n    });\n  });\n\n  it(\"should process address in Organization\", () => {\n    const orgWithAddress = {\n      name: \"Local Business\",\n      logo: \"https://example.com/logo.png\",\n      address: {\n        streetAddress: \"123 Business Ave\",\n        addressLocality: \"Businesstown\",\n        addressRegion: \"CA\",\n        postalCode: \"90210\",\n      },\n    };\n    const result = processAuthor(orgWithAddress);\n    expect(result).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Local Business\",\n      logo: \"https://example.com/logo.png\",\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"123 Business Ave\",\n        addressLocality: \"Businesstown\",\n        addressRegion: \"CA\",\n        postalCode: \"90210\",\n      },\n    });\n  });\n\n  it(\"should process logo as ImageObject in Organization\", () => {\n    const orgWithImageLogo = {\n      name: \"Visual Corp\",\n      logo: {\n        url: \"https://example.com/logo.png\",\n        width: 300,\n        height: 100,\n      },\n    };\n    const result = processAuthor(orgWithImageLogo);\n    expect(result).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Visual Corp\",\n      logo: {\n        \"@type\": \"ImageObject\",\n        url: \"https://example.com/logo.png\",\n        width: 300,\n        height: 100,\n      },\n    });\n  });\n});\n\ndescribe(\"processAddress\", () => {\n  it(\"should convert string to PostalAddress\", () => {\n    const result = processAddress(\"123 Main St\");\n    expect(result).toEqual({\n      \"@type\": \"PostalAddress\",\n      streetAddress: \"123 Main St\",\n    });\n  });\n\n  it(\"should return PostalAddress with @type as-is\", () => {\n    const address = {\n      \"@type\": \"PostalAddress\" as const,\n      streetAddress: \"123 Main St\",\n      addressLocality: \"New York\",\n      addressRegion: \"NY\",\n      postalCode: \"10001\",\n    };\n    const result = processAddress(address);\n    expect(result).toEqual(address);\n  });\n\n  it(\"should add @type to PostalAddress without it\", () => {\n    const addressWithoutType = {\n      streetAddress: \"456 Oak Ave\",\n      addressLocality: \"Los Angeles\",\n      addressRegion: \"CA\",\n      postalCode: \"90001\",\n    };\n    const result = processAddress(addressWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"PostalAddress\",\n      streetAddress: \"456 Oak Ave\",\n      addressLocality: \"Los Angeles\",\n      addressRegion: \"CA\",\n      postalCode: \"90001\",\n    });\n  });\n});\n\ndescribe(\"processPerformer\", () => {\n  it(\"should convert string to PerformingGroup\", () => {\n    const result = processPerformer(\"The Beatles\");\n    expect(result).toEqual({\n      \"@type\": \"PerformingGroup\",\n      name: \"The Beatles\",\n    });\n  });\n\n  it(\"should return Person with @type as-is\", () => {\n    const person = {\n      \"@type\": \"Person\" as const,\n      name: \"John Lennon\",\n      givenName: \"John\",\n      familyName: \"Lennon\",\n    };\n    const result = processPerformer(person);\n    expect(result).toEqual(person);\n  });\n\n  it(\"should return PerformingGroup with @type as-is\", () => {\n    const group = {\n      \"@type\": \"PerformingGroup\" as const,\n      name: \"The Rolling Stones\",\n    };\n    const result = processPerformer(group);\n    expect(result).toEqual(group);\n  });\n\n  it(\"should add @type Person when object has person properties\", () => {\n    const personWithoutType = {\n      name: \"Paul McCartney\",\n      givenName: \"Paul\",\n      familyName: \"McCartney\",\n    };\n    const result = processPerformer(personWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"Person\",\n      name: \"Paul McCartney\",\n      givenName: \"Paul\",\n      familyName: \"McCartney\",\n    });\n  });\n\n  it(\"should add @type PerformingGroup for object without person properties\", () => {\n    const groupWithoutType = {\n      name: \"Queen\",\n      description: \"British rock band\",\n    };\n    const result = processPerformer(groupWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"PerformingGroup\",\n      name: \"Queen\",\n      description: \"British rock band\",\n    });\n  });\n});\n\ndescribe(\"processContactPoint\", () => {\n  it(\"should return ContactPoint with @type as-is\", () => {\n    const contactPoint = {\n      \"@type\": \"ContactPoint\" as const,\n      contactType: \"customer service\",\n      telephone: \"+1-800-123-4567\",\n      email: \"support@example.com\",\n    };\n    const result = processContactPoint(contactPoint);\n    expect(result).toEqual(contactPoint);\n  });\n\n  it(\"should add @type to ContactPoint without it\", () => {\n    const contactPointWithoutType = {\n      contactType: \"sales\",\n      telephone: \"+1-800-555-1234\",\n    };\n    const result = processContactPoint(contactPointWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"ContactPoint\",\n      contactType: \"sales\",\n      telephone: \"+1-800-555-1234\",\n    });\n  });\n});\n\ndescribe(\"processOrganizer\", () => {\n  it(\"should convert string to Organization\", () => {\n    const result = processOrganizer(\"Event Company Inc\");\n    expect(result).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Event Company Inc\",\n    });\n  });\n\n  it(\"should add @type Person when object has person properties\", () => {\n    const personWithoutType = {\n      name: \"Jane Organizer\",\n      givenName: \"Jane\",\n    };\n    const result = processOrganizer(personWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"Person\",\n      name: \"Jane Organizer\",\n      givenName: \"Jane\",\n    });\n  });\n\n  it(\"should add @type Organization when object has no person properties\", () => {\n    const orgWithoutType = {\n      name: \"Event Planners LLC\",\n      url: \"https://eventplanners.com\",\n    };\n    const result = processOrganizer(orgWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Event Planners LLC\",\n      url: \"https://eventplanners.com\",\n    });\n  });\n});\n\ndescribe(\"processReview\", () => {\n  it(\"should add @type to Review without it\", () => {\n    const reviewWithoutType = {\n      author: \"John Reviewer\",\n      reviewBody: \"Great service!\",\n      reviewRating: {\n        ratingValue: 5,\n        bestRating: 5,\n      },\n    };\n    const result = processReview(reviewWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"Review\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"John Reviewer\",\n      },\n      reviewBody: \"Great service!\",\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 5,\n        bestRating: 5,\n      },\n    });\n  });\n\n  it(\"should preserve @type when Review already has it\", () => {\n    const reviewWithType = {\n      \"@type\": \"Review\" as const,\n      author: \"Jane Reviewer\",\n      reviewBody: \"Excellent!\",\n    };\n    const result = processReview(reviewWithType);\n    expect(result).toEqual({\n      \"@type\": \"Review\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"Jane Reviewer\",\n      },\n      reviewBody: \"Excellent!\",\n    });\n  });\n});\n\ndescribe(\"processMainEntityOfPage\", () => {\n  it(\"should return string URL as-is\", () => {\n    const result = processMainEntityOfPage(\"https://example.com/article\");\n    expect(result).toBe(\"https://example.com/article\");\n  });\n\n  it(\"should return WebPage with @type as-is\", () => {\n    const webPage = {\n      \"@type\": \"WebPage\" as const,\n      \"@id\": \"https://example.com/page\",\n    };\n    const result = processMainEntityOfPage(webPage);\n    expect(result).toEqual(webPage);\n  });\n\n  it(\"should add @type to WebPage without it\", () => {\n    const webPageWithoutType = {\n      \"@id\": \"https://example.com/page\",\n    };\n    const result = processMainEntityOfPage(webPageWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"WebPage\",\n      \"@id\": \"https://example.com/page\",\n    });\n  });\n});\n\ndescribe(\"processMerchantReturnPolicy\", () => {\n  it(\"should return policy with @type as-is\", () => {\n    const policy = {\n      \"@type\": \"MerchantReturnPolicy\" as const,\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 30,\n    };\n    const result = processMerchantReturnPolicy(policy);\n    expect(result).toEqual(policy);\n  });\n\n  it(\"should add @type to policy without it\", () => {\n    const policyWithoutType = {\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 14,\n      returnFees: \"https://schema.org/FreeReturn\",\n    };\n    const result = processMerchantReturnPolicy(policyWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"MerchantReturnPolicy\",\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 14,\n      returnFees: \"https://schema.org/FreeReturn\",\n    });\n  });\n});\n\ndescribe(\"processVideo\", () => {\n  it(\"should return video with @type as-is\", () => {\n    const video = {\n      \"@type\": \"VideoObject\" as const,\n      name: \"How to Cook\",\n      description: \"Cooking tutorial\",\n      thumbnailUrl: \"https://example.com/thumb.jpg\",\n      uploadDate: \"2024-01-01T00:00:00Z\",\n    };\n    const result = processVideo(video);\n    expect(result).toEqual(video);\n  });\n\n  it(\"should add @type to video without it\", () => {\n    const videoWithoutType = {\n      name: \"Recipe Video\",\n      description: \"Step by step guide\",\n      thumbnailUrl: \"https://example.com/video-thumb.jpg\",\n      uploadDate: \"2024-01-15T00:00:00Z\",\n    };\n    const result = processVideo(videoWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"VideoObject\",\n      name: \"Recipe Video\",\n      description: \"Step by step guide\",\n      thumbnailUrl: \"https://example.com/video-thumb.jpg\",\n      uploadDate: \"2024-01-15T00:00:00Z\",\n    });\n  });\n});\n\ndescribe(\"processInstruction\", () => {\n  it(\"should return string instruction as-is\", () => {\n    const result = processInstruction(\"Mix all ingredients\");\n    expect(result).toBe(\"Mix all ingredients\");\n  });\n\n  it(\"should add @type to HowToStep without it\", () => {\n    const stepWithoutType = {\n      text: \"Preheat oven to 350°F\",\n      name: \"Preheat\",\n    };\n    const result = processInstruction(stepWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"HowToStep\",\n      text: \"Preheat oven to 350°F\",\n      name: \"Preheat\",\n    });\n  });\n\n  it(\"should add @type to HowToSection without it\", () => {\n    const sectionWithoutType = {\n      name: \"Preparation\",\n      itemListElement: [\n        {\n          text: \"Gather ingredients\",\n        },\n        {\n          text: \"Prep workspace\",\n        },\n      ],\n    };\n    const result = processInstruction(sectionWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"HowToSection\",\n      name: \"Preparation\",\n      itemListElement: [\n        {\n          \"@type\": \"HowToStep\",\n          text: \"Gather ingredients\",\n        },\n        {\n          \"@type\": \"HowToStep\",\n          text: \"Prep workspace\",\n        },\n      ],\n    });\n  });\n\n  it(\"should preserve @type and process nested items in HowToSection\", () => {\n    const sectionWithType = {\n      \"@type\": \"HowToSection\" as const,\n      name: \"Cooking\",\n      itemListElement: [\n        {\n          text: \"Heat oil\",\n        },\n        {\n          \"@type\": \"HowToStep\" as const,\n          text: \"Add ingredients\",\n          name: \"Add\",\n        },\n      ],\n    };\n    const result = processInstruction(sectionWithType);\n    expect(result).toEqual({\n      \"@type\": \"HowToSection\",\n      name: \"Cooking\",\n      itemListElement: [\n        {\n          \"@type\": \"HowToStep\",\n          text: \"Heat oil\",\n        },\n        {\n          \"@type\": \"HowToStep\",\n          text: \"Add ingredients\",\n          name: \"Add\",\n        },\n      ],\n    });\n  });\n});\n\ndescribe(\"processDataCatalog\", () => {\n  it(\"should add @type to DataCatalog without it\", () => {\n    const catalogWithoutType = {\n      name: \"Ocean Climate Data Catalog\",\n      description: \"Collection of ocean climate datasets\",\n      url: \"https://example.com/catalog\",\n    };\n    const result = processDataCatalog(catalogWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"DataCatalog\",\n      name: \"Ocean Climate Data Catalog\",\n      description: \"Collection of ocean climate datasets\",\n      url: \"https://example.com/catalog\",\n    });\n  });\n\n  it(\"should preserve @type when DataCatalog already has it\", () => {\n    const catalogWithType = {\n      \"@type\": \"DataCatalog\" as const,\n      name: \"Climate Data Repository\",\n      description: \"Global climate datasets\",\n      url: \"https://climate.example.com\",\n    };\n    const result = processDataCatalog(catalogWithType);\n    expect(result).toEqual(catalogWithType);\n  });\n\n  it(\"should handle DataCatalog with nested Dataset\", () => {\n    const catalogWithDataset = {\n      name: \"Environmental Data Catalog\",\n      hasPart: {\n        \"@type\": \"Dataset\" as const,\n        name: \"Air Quality Dataset\",\n        description: \"Air quality measurements\",\n      },\n    };\n    const result = processDataCatalog(catalogWithDataset);\n    expect(result).toEqual({\n      \"@type\": \"DataCatalog\",\n      name: \"Environmental Data Catalog\",\n      hasPart: {\n        \"@type\": \"Dataset\",\n        name: \"Air Quality Dataset\",\n        description: \"Air quality measurements\",\n      },\n    });\n  });\n\n  it(\"should handle DataCatalog with array of Datasets\", () => {\n    const catalogWithMultipleDatasets = {\n      name: \"Multi-Dataset Catalog\",\n      hasPart: [\n        {\n          \"@type\": \"Dataset\" as const,\n          name: \"Dataset 1\",\n          description: \"First dataset\",\n        },\n        {\n          \"@type\": \"Dataset\" as const,\n          name: \"Dataset 2\",\n          description: \"Second dataset\",\n        },\n      ],\n    };\n    const result = processDataCatalog(catalogWithMultipleDatasets);\n    expect(result).toEqual({\n      \"@type\": \"DataCatalog\",\n      name: \"Multi-Dataset Catalog\",\n      hasPart: [\n        {\n          \"@type\": \"Dataset\",\n          name: \"Dataset 1\",\n          description: \"First dataset\",\n        },\n        {\n          \"@type\": \"Dataset\",\n          name: \"Dataset 2\",\n          description: \"Second dataset\",\n        },\n      ],\n    });\n  });\n});\n\ndescribe(\"processDataDownload\", () => {\n  it(\"should return DataDownload with @type as-is\", () => {\n    const downloadWithType = {\n      \"@type\": \"DataDownload\" as const,\n      encodingFormat: \"application/csv\",\n      contentUrl: \"https://example.com/data.csv\",\n    };\n    const result = processDataDownload(downloadWithType);\n    expect(result).toBe(downloadWithType);\n  });\n\n  it(\"should add @type to DataDownload without it\", () => {\n    const downloadWithoutType = {\n      encodingFormat: \"application/json\",\n      contentUrl: \"https://example.com/data.json\",\n      contentSize: \"5MB\",\n    };\n    const result = processDataDownload(downloadWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"DataDownload\",\n      encodingFormat: \"application/json\",\n      contentUrl: \"https://example.com/data.json\",\n      contentSize: \"5MB\",\n    });\n  });\n});\n\ndescribe(\"processLicense\", () => {\n  it(\"should return string license as-is\", () => {\n    const result = processLicense(\n      \"https://creativecommons.org/licenses/by/4.0/\",\n    );\n    expect(result).toBe(\"https://creativecommons.org/licenses/by/4.0/\");\n  });\n\n  it(\"should return CreativeWork with @type as-is\", () => {\n    const licenseWithType = {\n      \"@type\": \"CreativeWork\" as const,\n      name: \"Creative Commons Attribution 4.0\",\n      url: \"https://creativecommons.org/licenses/by/4.0/\",\n    };\n    const result = processLicense(licenseWithType);\n    expect(result).toBe(licenseWithType);\n  });\n\n  it(\"should add @type to CreativeWork without it\", () => {\n    const licenseWithoutType = {\n      name: \"MIT License\",\n      url: \"https://opensource.org/licenses/MIT\",\n      text: \"Permission is hereby granted...\",\n    };\n    const result = processLicense(licenseWithoutType);\n    expect(result).toEqual({\n      \"@type\": \"CreativeWork\",\n      name: \"MIT License\",\n      url: \"https://opensource.org/licenses/MIT\",\n      text: \"Permission is hereby granted...\",\n    });\n  });\n});\n"
  },
  {
    "path": "src/utils/processors.ts",
    "content": "import type {\n  Author,\n  Person,\n  Organization,\n  ImageObject,\n  PostalAddress,\n  ContactPoint,\n  QuantitativeValue,\n  GeoCoordinates,\n  OpeningHoursSpecification,\n  Review,\n  AggregateRating,\n  MerchantReturnPolicy,\n  MerchantReturnPolicySeasonalOverride,\n  SimpleMonetaryAmount,\n  Rating,\n  VideoObject,\n  InteractionCounter,\n  Brand,\n  BedDetails,\n  LocationFeatureSpecification,\n  Accommodation,\n  MemberProgram,\n  MemberProgramTier,\n  CreditCard,\n  UnitPriceSpecification,\n  TierRequirement,\n  TierBenefit,\n  Certification,\n  PeopleAudience,\n  SizeSpecification,\n  ThreeDModel,\n  DefinedRegion,\n  ShippingDeliveryTime,\n  OfferShippingDetails,\n} from \"~/types/common.types\";\nimport type { Director } from \"~/types/movie-carousel.types\";\nimport type { Provider } from \"~/types/course.types\";\nimport type { BreadcrumbListItem, ListItem } from \"~/types/breadcrumb.types\";\nimport type {\n  Place,\n  Performer,\n  Organizer,\n  Offer,\n  PerformingGroup,\n} from \"~/types/event.types\";\nimport type {\n  NutritionInformation,\n  HowToStep,\n  HowToSection,\n} from \"~/types/recipe.types\";\nimport type {\n  GeoShape,\n  PropertyValue,\n  CreativeWork,\n  DatasetPlace,\n  DataDownload,\n  DataCatalog,\n} from \"~/types/dataset.types\";\nimport type {\n  Place as JobPlace,\n  PropertyValue as JobPropertyValue,\n  MonetaryAmount,\n  Country,\n  State,\n  AdministrativeArea,\n  EducationalOccupationalCredential,\n  OccupationalExperienceRequirements,\n} from \"~/types/jobposting.types\";\nimport type {\n  Comment,\n  SharedContent,\n  WebPage as ForumWebPage,\n  CreativeWork as ForumCreativeWork,\n} from \"~/types/discussionforum.types\";\nimport type {\n  Claim,\n  ClaimReviewRating,\n  ClaimCreativeWork,\n} from \"~/types/claimreview.types\";\nimport type {\n  BroadcastEvent,\n  Clip,\n  PotentialAction,\n} from \"~/types/video.types\";\nimport type { WebPageElement } from \"~/types/creativework.types\";\nimport type {\n  ProductOffer,\n  AggregateOffer,\n  PriceSpecification,\n  ProductItemList,\n  ProductListItem,\n  ProductReview,\n  Product,\n  VariesBy,\n} from \"~/types/product.types\";\nimport type {\n  HowToSupply,\n  HowToTool,\n  HowToDirection,\n  HowToTip,\n  HowToStep as HowToStepType,\n  HowToSection as HowToSectionType,\n  Step,\n  Supply,\n  Tool,\n  EstimatedCost,\n  HowToYield,\n} from \"~/types/howto.types\";\n\n// Schema.org type constants\nconst SCHEMA_TYPES = {\n  PERSON: \"Person\",\n  ORGANIZATION: \"Organization\",\n  IMAGE_OBJECT: \"ImageObject\",\n  POSTAL_ADDRESS: \"PostalAddress\",\n  CONTACT_POINT: \"ContactPoint\",\n  QUANTITATIVE_VALUE: \"QuantitativeValue\",\n  GEO_COORDINATES: \"GeoCoordinates\",\n  GEO_SHAPE: \"GeoShape\",\n  OPENING_HOURS: \"OpeningHoursSpecification\",\n  REVIEW: \"Review\",\n  RATING: \"Rating\",\n  AGGREGATE_RATING: \"AggregateRating\",\n  MERCHANT_RETURN_POLICY: \"MerchantReturnPolicy\",\n  MERCHANT_RETURN_POLICY_SEASONAL_OVERRIDE:\n    \"MerchantReturnPolicySeasonalOverride\",\n  MONETARY_AMOUNT: \"MonetaryAmount\",\n  VIDEO_OBJECT: \"VideoObject\",\n  INTERACTION_COUNTER: \"InteractionCounter\",\n  BRAND: \"Brand\",\n  CREDIT_CARD: \"CreditCard\",\n  UNIT_PRICE_SPECIFICATION: \"UnitPriceSpecification\",\n  MEMBER_PROGRAM: \"MemberProgram\",\n  MEMBER_PROGRAM_TIER: \"MemberProgramTier\",\n  BED_DETAILS: \"BedDetails\",\n  LOCATION_FEATURE: \"LocationFeatureSpecification\",\n  ACCOMMODATION: \"Accommodation\",\n  PLACE: \"Place\",\n  PERFORMING_GROUP: \"PerformingGroup\",\n  OFFER: \"Offer\",\n  AGGREGATE_OFFER: \"AggregateOffer\",\n  PRICE_SPECIFICATION: \"PriceSpecification\",\n  ITEM_LIST: \"ItemList\",\n  LIST_ITEM: \"ListItem\",\n  PRODUCT: \"Product\",\n  PRODUCT_GROUP: \"ProductGroup\",\n  NUTRITION_INFORMATION: \"NutritionInformation\",\n  HOW_TO_STEP: \"HowToStep\",\n  HOW_TO_SECTION: \"HowToSection\",\n  HOW_TO_SUPPLY: \"HowToSupply\",\n  HOW_TO_TOOL: \"HowToTool\",\n  HOW_TO_DIRECTION: \"HowToDirection\",\n  HOW_TO_TIP: \"HowToTip\",\n  PROPERTY_VALUE: \"PropertyValue\",\n  CREATIVE_WORK: \"CreativeWork\",\n  DATA_DOWNLOAD: \"DataDownload\",\n  DATA_CATALOG: \"DataCatalog\",\n  COUNTRY: \"Country\",\n  STATE: \"State\",\n  EDUCATIONAL_CREDENTIAL: \"EducationalOccupationalCredential\",\n  OCCUPATIONAL_EXPERIENCE: \"OccupationalExperienceRequirements\",\n  COMMENT: \"Comment\",\n  WEB_PAGE: \"WebPage\",\n  WEB_PAGE_ELEMENT: \"WebPageElement\",\n  CLAIM: \"Claim\",\n  CERTIFICATION: \"Certification\",\n  PEOPLE_AUDIENCE: \"PeopleAudience\",\n  SIZE_SPECIFICATION: \"SizeSpecification\",\n  THREE_D_MODEL: \"3DModel\",\n  DEFINED_REGION: \"DefinedRegion\",\n  SHIPPING_DELIVERY_TIME: \"ShippingDeliveryTime\",\n  OFFER_SHIPPING_DETAILS: \"OfferShippingDetails\",\n} as const;\n\n// Type guard utilities\nfunction hasType<T extends { \"@type\": string }>(obj: unknown): obj is T {\n  return obj !== null && typeof obj === \"object\" && \"@type\" in obj;\n}\n\nfunction isString(value: unknown): value is string {\n  return typeof value === \"string\";\n}\n\n// Generic processor for simple schema types\nexport function processSchemaType<T extends { \"@type\": string }>(\n  value: unknown,\n  schemaType: string,\n  stringHandler?: (str: string) => Omit<T, \"@type\">,\n  numberHandler?: (num: number) => Omit<T, \"@type\">,\n): T {\n  if (isString(value) && stringHandler) {\n    return { \"@type\": schemaType, ...stringHandler(value) } as T;\n  }\n\n  if (typeof value === \"number\" && numberHandler) {\n    return { \"@type\": schemaType, ...numberHandler(value) } as T;\n  }\n\n  if (hasType<T>(value)) {\n    return value;\n  }\n\n  // Ensure value is an object before spreading\n  if (typeof value === \"object\" && value !== null) {\n    return { \"@type\": schemaType, ...value } as T;\n  }\n\n  // Fallback for non-object values\n  return { \"@type\": schemaType } as T;\n}\n\n// Helper to process nested organization fields\nfunction processOrganizationFields(org: Organization): void {\n  if (org.logo && !isString(org.logo)) {\n    org.logo = processLogo(org.logo);\n  }\n\n  if (org.address && !isString(org.address)) {\n    if (Array.isArray(org.address)) {\n      org.address = org.address.map((addr) =>\n        isString(addr) ? addr : processAddress(addr),\n      );\n    } else {\n      org.address = processAddress(org.address);\n    }\n  }\n\n  if (org.contactPoint) {\n    if (Array.isArray(org.contactPoint)) {\n      org.contactPoint = org.contactPoint.map(processContactPoint);\n    } else {\n      org.contactPoint = processContactPoint(org.contactPoint);\n    }\n  }\n}\n\n/**\n * Processes author input into a Person or Organization schema type\n * @param author - String name, Person object, or Organization object\n * @returns Processed Person or Organization with @type\n * @example\n * processAuthor(\"John Doe\") // { \"@type\": \"Person\", name: \"John Doe\" }\n * processAuthor({ name: \"ACME Corp\", logo: \"logo.jpg\" }) // { \"@type\": \"Organization\", ... }\n */\nexport function processAuthor(author: Author): Person | Organization {\n  if (isString(author)) {\n    // Check if the string appears to be an organization name\n    const orgIndicators = [\n      \"magazine\",\n      \"publication\",\n      \"company\",\n      \"corporation\",\n      \"corp\",\n      \"inc\",\n      \"llc\",\n      \"ltd\",\n      \"limited\",\n      \"group\",\n      \"foundation\",\n      \"institute\",\n      \"association\",\n      \"society\",\n      \"union\",\n      \"times\",\n      \"news\",\n      \"press\",\n      \"media\",\n      \"network\",\n      \"agency\",\n      \"studio\",\n    ];\n\n    const lowerName = author.toLowerCase();\n    const isLikelyOrg = orgIndicators.some((indicator) =>\n      lowerName.includes(indicator),\n    );\n\n    if (isLikelyOrg) {\n      return {\n        \"@type\": SCHEMA_TYPES.ORGANIZATION,\n        name: author,\n      };\n    }\n\n    return {\n      \"@type\": SCHEMA_TYPES.PERSON,\n      name: author,\n    };\n  }\n\n  if (hasType<Person | Organization>(author)) {\n    return author;\n  }\n\n  // Determine if it's Person or Organization based on properties\n  const hasOrgProperties =\n    \"logo\" in author || \"address\" in author || \"contactPoint\" in author;\n\n  if (hasOrgProperties) {\n    const org: Organization = {\n      \"@type\": SCHEMA_TYPES.ORGANIZATION,\n      ...author,\n    };\n    processOrganizationFields(org);\n    return org;\n  }\n\n  // Default to Person\n  return {\n    \"@type\": SCHEMA_TYPES.PERSON,\n    ...author,\n  } as Person;\n}\n\n/**\n * Processes image input into ImageObject schema type\n * @param image - URL string or ImageObject\n * @returns URL string or ImageObject with @type\n * @example\n * processImage(\"https://example.com/image.jpg\") // \"https://example.com/image.jpg\"\n * processImage({ url: \"image.jpg\", width: 800 }) // { \"@type\": \"ImageObject\", ... }\n */\nexport function processImage(\n  image: string | ImageObject | Omit<ImageObject, \"@type\">,\n): string | ImageObject {\n  if (isString(image)) {\n    return image;\n  }\n\n  return processSchemaType<ImageObject>(image, SCHEMA_TYPES.IMAGE_OBJECT);\n}\n\n/**\n * Processes address input into PostalAddress schema type\n * @param address - String address or PostalAddress object\n * @returns PostalAddress with @type\n * @example\n * processAddress(\"123 Main St\") // { \"@type\": \"PostalAddress\", streetAddress: \"123 Main St\" }\n */\nexport function processAddress(\n  address: string | PostalAddress | Omit<PostalAddress, \"@type\">,\n): PostalAddress {\n  return processSchemaType<PostalAddress>(\n    address,\n    SCHEMA_TYPES.POSTAL_ADDRESS,\n    (str) => ({ streetAddress: str }),\n    undefined,\n  );\n}\n\n/**\n * Processes contact point into ContactPoint schema type\n * @param contactPoint - ContactPoint object with or without @type\n * @returns ContactPoint with @type\n */\nexport function processContactPoint(\n  contactPoint: ContactPoint | Omit<ContactPoint, \"@type\">,\n): ContactPoint {\n  return processSchemaType<ContactPoint>(\n    contactPoint,\n    SCHEMA_TYPES.CONTACT_POINT,\n  );\n}\n\n/**\n * Processes logo the same way as images\n * @param logo - URL string or ImageObject\n * @returns URL string or ImageObject with @type\n */\nexport function processLogo(\n  logo: string | ImageObject | Omit<ImageObject, \"@type\">,\n): string | ImageObject {\n  return processImage(logo);\n}\n\n/**\n * Processes number of employees into QuantitativeValue schema type\n * @param numberOfEmployees - Number or QuantitativeValue object\n * @returns QuantitativeValue with @type\n */\nexport function processNumberOfEmployees(\n  numberOfEmployees:\n    | number\n    | QuantitativeValue\n    | Omit<QuantitativeValue, \"@type\">,\n): QuantitativeValue {\n  return processSchemaType<QuantitativeValue>(\n    numberOfEmployees,\n    SCHEMA_TYPES.QUANTITATIVE_VALUE,\n    undefined,\n    (num) => ({ value: num }),\n  );\n}\n\n/**\n * Processes geographic coordinates into GeoCoordinates schema type\n * @param geo - GeoCoordinates object with or without @type\n * @returns GeoCoordinates with @type\n */\nexport function processGeo(\n  geo: GeoCoordinates | Omit<GeoCoordinates, \"@type\">,\n): GeoCoordinates {\n  return processSchemaType<GeoCoordinates>(geo, SCHEMA_TYPES.GEO_COORDINATES);\n}\n\n/**\n * Processes opening hours into OpeningHoursSpecification schema type\n * @param hours - OpeningHoursSpecification object with or without @type\n * @returns OpeningHoursSpecification with @type\n */\nexport function processOpeningHours(\n  hours: OpeningHoursSpecification | Omit<OpeningHoursSpecification, \"@type\">,\n): OpeningHoursSpecification {\n  return processSchemaType<OpeningHoursSpecification>(\n    hours,\n    SCHEMA_TYPES.OPENING_HOURS,\n  );\n}\n\n/**\n * Processes review into Review schema type with nested rating processing\n * @param review - Review object with or without @type\n * @returns Review with @type and processed nested fields\n */\nexport function processReview(review: Review | Omit<Review, \"@type\">): Review {\n  const processed: Review = processSchemaType<Review>(\n    review,\n    SCHEMA_TYPES.REVIEW,\n  );\n\n  // Process nested rating\n  if (review.reviewRating) {\n    processed.reviewRating = processSchemaType<Rating>(\n      review.reviewRating,\n      SCHEMA_TYPES.RATING,\n    );\n  }\n\n  // Process nested author\n  if (review.author) {\n    processed.author = processAuthor(review.author);\n  }\n\n  return processed;\n}\n\n/**\n * Processes breadcrumb item into ListItem schema type\n * @param item - BreadcrumbListItem object\n * @param position - Position in the breadcrumb trail\n * @returns ListItem with @type and position\n */\nexport function processBreadcrumbItem(\n  item: BreadcrumbListItem,\n  position: number,\n): ListItem {\n  return {\n    \"@type\": SCHEMA_TYPES.LIST_ITEM,\n    position,\n    ...(item.name && { name: item.name }),\n    ...(item.item && { item: item.item }),\n  };\n}\n\n/**\n * Processes location/place into Place schema type\n * @param location - String location or Place object\n * @returns Place with @type\n */\nexport function processPlace(\n  location: string | Place | Omit<Place, \"@type\">,\n): Place {\n  return processSchemaType<Place>(\n    location,\n    SCHEMA_TYPES.PLACE,\n    (str) => ({\n      name: str,\n      address: {\n        \"@type\": SCHEMA_TYPES.POSTAL_ADDRESS,\n        streetAddress: str,\n      },\n    }),\n    undefined,\n  );\n}\n\n/**\n * Processes performer into Person or PerformingGroup schema type\n * @param performer - String name or Performer object\n * @returns Person or PerformingGroup with @type\n */\nexport function processPerformer(\n  performer: Performer,\n): Person | PerformingGroup {\n  if (isString(performer)) {\n    return {\n      \"@type\": SCHEMA_TYPES.PERFORMING_GROUP,\n      name: performer,\n    };\n  }\n\n  if (hasType<Person | PerformingGroup>(performer)) {\n    return performer;\n  }\n\n  // Check for Person-specific properties\n  const hasPersonProperties =\n    \"familyName\" in performer ||\n    \"givenName\" in performer ||\n    \"additionalName\" in performer;\n\n  return hasPersonProperties\n    ? ({ \"@type\": SCHEMA_TYPES.PERSON, ...performer } as Person)\n    : ({\n        \"@type\": SCHEMA_TYPES.PERFORMING_GROUP,\n        ...performer,\n      } as PerformingGroup);\n}\n\n/**\n * Processes organizer into Person or Organization schema type\n * @param organizer - String name or Organizer object\n * @returns Person or Organization with @type\n */\nexport function processOrganizer(organizer: Organizer): Person | Organization {\n  if (isString(organizer)) {\n    return {\n      \"@type\": SCHEMA_TYPES.ORGANIZATION,\n      name: organizer,\n    };\n  }\n\n  if (hasType<Person | Organization>(organizer)) {\n    return organizer;\n  }\n\n  // Check for Person-specific properties\n  const hasPersonProperties =\n    \"familyName\" in organizer ||\n    \"givenName\" in organizer ||\n    \"additionalName\" in organizer;\n\n  return hasPersonProperties\n    ? ({ \"@type\": SCHEMA_TYPES.PERSON, ...organizer } as Person)\n    : ({ \"@type\": SCHEMA_TYPES.ORGANIZATION, ...organizer } as Organization);\n}\n\n/**\n * Processes generic organization input into Organization schema type\n * @param org - String name or Organization object\n * @returns Organization with @type and processed nested fields\n */\nexport function processOrganization(\n  org: string | Organization | Omit<Organization, \"@type\">,\n): Organization {\n  if (isString(org)) {\n    return {\n      \"@type\": SCHEMA_TYPES.ORGANIZATION,\n      name: org,\n    };\n  }\n\n  const processed = processSchemaType<Organization>(\n    org,\n    SCHEMA_TYPES.ORGANIZATION,\n  );\n\n  // Process nested fields if present\n  processOrganizationFields(processed);\n\n  return processed;\n}\n\n/**\n * Processes offer into Offer schema type\n * @param offer - Offer object with or without @type\n * @returns Offer with @type\n */\nexport function processOffer(offer: Offer | Omit<Offer, \"@type\">): Offer {\n  return processSchemaType<Offer>(offer, SCHEMA_TYPES.OFFER);\n}\n\n/**\n * Processes publisher into Person or Organization schema type\n * @param publisher - String name, Person, or Organization object\n * @returns Person or Organization with @type and processed nested fields\n */\nexport function processPublisher(\n  publisher:\n    | string\n    | Organization\n    | Person\n    | Omit<Organization, \"@type\">\n    | Omit<Person, \"@type\">,\n): Person | Organization {\n  if (isString(publisher)) {\n    return {\n      \"@type\": SCHEMA_TYPES.ORGANIZATION,\n      name: publisher,\n    };\n  }\n\n  if (\n    hasType<Organization>(publisher) &&\n    publisher[\"@type\"] === SCHEMA_TYPES.ORGANIZATION\n  ) {\n    const org = { ...publisher };\n    processOrganizationFields(org);\n    return org;\n  }\n\n  if (hasType<Person | Organization>(publisher)) {\n    return publisher;\n  }\n\n  // Default to Organization for publishers\n  const org: Organization = {\n    \"@type\": SCHEMA_TYPES.ORGANIZATION,\n    ...publisher,\n  };\n  processOrganizationFields(org);\n  return org;\n}\n\n/**\n * Processes nutrition information into NutritionInformation schema type\n * @param nutrition - NutritionInformation object without @type\n * @returns NutritionInformation with @type\n */\nexport function processNutrition(\n  nutrition: Omit<NutritionInformation, \"@type\">,\n): NutritionInformation {\n  return {\n    \"@type\": SCHEMA_TYPES.NUTRITION_INFORMATION,\n    ...nutrition,\n  };\n}\n\n/**\n * Processes aggregate rating into AggregateRating schema type\n * @param rating - AggregateRating object with or without @type\n * @returns AggregateRating with @type\n */\nexport function processAggregateRating(\n  rating: AggregateRating | Omit<AggregateRating, \"@type\">,\n): AggregateRating {\n  return processSchemaType<AggregateRating>(\n    rating,\n    SCHEMA_TYPES.AGGREGATE_RATING,\n  );\n}\n\ntype WebPage = {\n  \"@type\": \"WebPage\";\n  \"@id\": string;\n};\n\n/**\n * Processes main entity of page into string URL or WebPage schema type\n * @param mainEntityOfPage - String URL or WebPage object\n * @returns String URL or WebPage with @type\n */\nexport function processMainEntityOfPage(\n  mainEntityOfPage: string | WebPage | Omit<WebPage, \"@type\">,\n): string | WebPage {\n  if (isString(mainEntityOfPage)) {\n    return mainEntityOfPage;\n  }\n\n  return processSchemaType<WebPage>(mainEntityOfPage, SCHEMA_TYPES.WEB_PAGE);\n}\n\n/**\n * Processes simple monetary amount into MonetaryAmount schema type for return policies\n * @param amount - SimpleMonetaryAmount object with or without @type, or a number\n * @returns SimpleMonetaryAmount with @type or undefined\n */\nexport function processSimpleMonetaryAmount(\n  amount:\n    | number\n    | SimpleMonetaryAmount\n    | Omit<SimpleMonetaryAmount, \"@type\">\n    | undefined,\n): SimpleMonetaryAmount | undefined {\n  if (!amount) return undefined;\n\n  // Handle number input\n  if (typeof amount === \"number\") {\n    return {\n      \"@type\": SCHEMA_TYPES.MONETARY_AMOUNT,\n      value: amount,\n      currency: \"USD\", // Default currency, should be overridden in component\n    };\n  }\n\n  return processSchemaType<SimpleMonetaryAmount>(\n    amount,\n    SCHEMA_TYPES.MONETARY_AMOUNT,\n  );\n}\n\n/**\n * Processes seasonal override into MerchantReturnPolicySeasonalOverride schema type\n * @param override - MerchantReturnPolicySeasonalOverride object with or without @type\n * @returns MerchantReturnPolicySeasonalOverride with @type\n */\nexport function processReturnPolicySeasonalOverride(\n  override:\n    | MerchantReturnPolicySeasonalOverride\n    | Omit<MerchantReturnPolicySeasonalOverride, \"@type\">,\n): MerchantReturnPolicySeasonalOverride {\n  return processSchemaType<MerchantReturnPolicySeasonalOverride>(\n    override,\n    SCHEMA_TYPES.MERCHANT_RETURN_POLICY_SEASONAL_OVERRIDE,\n  );\n}\n\n/**\n * Processes merchant return policy into MerchantReturnPolicy schema type\n * Enhanced to handle nested properties like MonetaryAmount and seasonal overrides\n * @param policy - MerchantReturnPolicy object with or without @type\n * @returns MerchantReturnPolicy with @type and processed nested properties\n */\nexport function processMerchantReturnPolicy(\n  policy: MerchantReturnPolicy | Omit<MerchantReturnPolicy, \"@type\">,\n): MerchantReturnPolicy {\n  if (!policy) return policy as MerchantReturnPolicy;\n\n  const processed = processSchemaType<MerchantReturnPolicy>(\n    policy,\n    SCHEMA_TYPES.MERCHANT_RETURN_POLICY,\n  );\n\n  // Normalize string values to arrays for consistency\n  if (\n    processed.applicableCountry &&\n    !Array.isArray(processed.applicableCountry)\n  ) {\n    processed.applicableCountry = [processed.applicableCountry];\n  }\n\n  if (\n    processed.returnPolicyCountry &&\n    !Array.isArray(processed.returnPolicyCountry)\n  ) {\n    processed.returnPolicyCountry = [processed.returnPolicyCountry];\n  }\n\n  if (processed.returnMethod && !Array.isArray(processed.returnMethod)) {\n    processed.returnMethod = [processed.returnMethod];\n  }\n\n  if (processed.refundType && !Array.isArray(processed.refundType)) {\n    processed.refundType = [processed.refundType];\n  }\n\n  if (processed.itemCondition && !Array.isArray(processed.itemCondition)) {\n    processed.itemCondition = [processed.itemCondition];\n  }\n\n  // Process nested MonetaryAmount fields\n  if (processed.returnShippingFeesAmount) {\n    processed.returnShippingFeesAmount = processSimpleMonetaryAmount(\n      processed.returnShippingFeesAmount,\n    );\n  }\n\n  if (processed.customerRemorseReturnShippingFeesAmount) {\n    processed.customerRemorseReturnShippingFeesAmount =\n      processSimpleMonetaryAmount(\n        processed.customerRemorseReturnShippingFeesAmount,\n      );\n  }\n\n  if (processed.itemDefectReturnShippingFeesAmount) {\n    processed.itemDefectReturnShippingFeesAmount = processSimpleMonetaryAmount(\n      processed.itemDefectReturnShippingFeesAmount,\n    );\n  }\n\n  // Process restocking fee (can be number or SimpleMonetaryAmount)\n  if (processed.restockingFee && typeof processed.restockingFee === \"object\") {\n    processed.restockingFee = processSimpleMonetaryAmount(\n      processed.restockingFee,\n    );\n  }\n\n  // Process seasonal overrides\n  if (processed.returnPolicySeasonalOverride) {\n    if (Array.isArray(processed.returnPolicySeasonalOverride)) {\n      processed.returnPolicySeasonalOverride =\n        processed.returnPolicySeasonalOverride.map(\n          processReturnPolicySeasonalOverride,\n        );\n    } else {\n      processed.returnPolicySeasonalOverride =\n        processReturnPolicySeasonalOverride(\n          processed.returnPolicySeasonalOverride,\n        );\n    }\n  }\n\n  return processed;\n}\n\n/**\n * Processes tier requirement into appropriate schema type\n * @param requirement - Tier requirement that can be CreditCard, MonetaryAmount, UnitPriceSpecification, or string\n * @returns Processed tier requirement with @type\n */\nexport function processTierRequirement(\n  requirement: TierRequirement,\n): TierRequirement {\n  if (!requirement) return requirement;\n\n  // If it's a string, return as-is (text description)\n  if (isString(requirement)) {\n    return requirement;\n  }\n\n  // If it already has @type, return as-is\n  if (hasType(requirement)) {\n    return requirement;\n  }\n\n  // Determine type based on properties\n  if (\"priceCurrency\" in requirement && \"price\" in requirement) {\n    // UnitPriceSpecification has both price and priceCurrency\n    if (\n      \"billingDuration\" in requirement ||\n      \"billingIncrement\" in requirement ||\n      \"unitCode\" in requirement\n    ) {\n      return {\n        \"@type\": SCHEMA_TYPES.UNIT_PRICE_SPECIFICATION,\n        ...requirement,\n      } as UnitPriceSpecification;\n    }\n  }\n\n  if (\"value\" in requirement && \"currency\" in requirement) {\n    // MonetaryAmount\n    return {\n      \"@type\": SCHEMA_TYPES.MONETARY_AMOUNT,\n      ...requirement,\n    } as SimpleMonetaryAmount;\n  }\n\n  if (\"name\" in requirement) {\n    // CreditCard\n    return {\n      \"@type\": SCHEMA_TYPES.CREDIT_CARD,\n      ...requirement,\n    } as CreditCard;\n  }\n\n  // Default to returning as-is if we can't determine the type\n  return requirement;\n}\n\n/**\n * Processes tier benefit, normalizing short names to full URLs\n * @param benefit - Tier benefit string or array\n * @returns Normalized tier benefit\n */\nexport function processTierBenefit(\n  benefit: TierBenefit | TierBenefit[],\n): TierBenefit | TierBenefit[] {\n  const normalizeBenefit = (b: TierBenefit): TierBenefit => {\n    // If it's already a full URL, return as-is\n    if (b.startsWith(\"https://schema.org/\")) {\n      return b;\n    }\n    // Convert short name to full URL\n    if (b === \"TierBenefitLoyaltyPoints\") {\n      return \"https://schema.org/TierBenefitLoyaltyPoints\";\n    }\n    if (b === \"TierBenefitLoyaltyPrice\") {\n      return \"https://schema.org/TierBenefitLoyaltyPrice\";\n    }\n    // Return as-is if unrecognized\n    return b;\n  };\n\n  if (Array.isArray(benefit)) {\n    return benefit.map(normalizeBenefit);\n  }\n  return normalizeBenefit(benefit);\n}\n\n/**\n * Processes membership points earned into QuantitativeValue\n * @param points - Number or QuantitativeValue\n * @returns QuantitativeValue with @type\n */\nexport function processMembershipPointsEarned(\n  points: number | QuantitativeValue | Omit<QuantitativeValue, \"@type\">,\n): QuantitativeValue {\n  if (typeof points === \"number\") {\n    return {\n      \"@type\": SCHEMA_TYPES.QUANTITATIVE_VALUE,\n      value: points,\n    };\n  }\n  return processSchemaType<QuantitativeValue>(\n    points,\n    SCHEMA_TYPES.QUANTITATIVE_VALUE,\n  );\n}\n\n/**\n * Processes member program tier into MemberProgramTier schema type\n * @param tier - MemberProgramTier with or without @type\n * @returns MemberProgramTier with @type\n */\nexport function processMemberProgramTier(\n  tier: MemberProgramTier | Omit<MemberProgramTier, \"@type\">,\n): MemberProgramTier {\n  const processed = processSchemaType<MemberProgramTier>(\n    tier,\n    SCHEMA_TYPES.MEMBER_PROGRAM_TIER,\n  );\n\n  // Process hasTierBenefit\n  if (processed.hasTierBenefit) {\n    processed.hasTierBenefit = processTierBenefit(processed.hasTierBenefit);\n  }\n\n  // Process hasTierRequirement\n  if (processed.hasTierRequirement) {\n    processed.hasTierRequirement = processTierRequirement(\n      processed.hasTierRequirement,\n    );\n  }\n\n  // Process membershipPointsEarned\n  if (processed.membershipPointsEarned !== undefined) {\n    processed.membershipPointsEarned = processMembershipPointsEarned(\n      processed.membershipPointsEarned,\n    );\n  }\n\n  return processed;\n}\n\n/**\n * Processes member program into MemberProgram schema type\n * @param program - MemberProgram with or without @type\n * @returns MemberProgram with @type\n */\nexport function processMemberProgram(\n  program: MemberProgram | Omit<MemberProgram, \"@type\">,\n): MemberProgram {\n  const processed = processSchemaType<MemberProgram>(\n    program,\n    SCHEMA_TYPES.MEMBER_PROGRAM,\n  );\n\n  // Process hasTiers\n  if (processed.hasTiers) {\n    if (Array.isArray(processed.hasTiers)) {\n      processed.hasTiers = processed.hasTiers.map(processMemberProgramTier);\n    } else {\n      processed.hasTiers = processMemberProgramTier(processed.hasTiers);\n    }\n  }\n\n  return processed;\n}\n\n/**\n * Processes video into VideoObject schema type\n * @param video - VideoObject with or without @type\n * @returns VideoObject with @type\n */\nexport function processVideo(\n  video: VideoObject | Omit<VideoObject, \"@type\">,\n): VideoObject {\n  return processSchemaType<VideoObject>(video, SCHEMA_TYPES.VIDEO_OBJECT);\n}\n\n/**\n * Processes broadcast event into BroadcastEvent schema type\n * @param broadcast - BroadcastEvent with or without @type\n * @returns BroadcastEvent with @type\n */\nexport function processBroadcastEvent(\n  broadcast: BroadcastEvent | Omit<BroadcastEvent, \"@type\">,\n): BroadcastEvent {\n  if (!broadcast) return broadcast as BroadcastEvent;\n\n  if (typeof broadcast === \"object\" && !(\"@type\" in broadcast)) {\n    return {\n      \"@type\": \"BroadcastEvent\",\n      ...broadcast,\n    };\n  }\n\n  return broadcast as BroadcastEvent;\n}\n\n/**\n * Processes clip into Clip schema type\n * @param clip - Clip with or without @type\n * @returns Clip with @type\n */\nexport function processClip(clip: Clip | Omit<Clip, \"@type\">): Clip {\n  if (!clip) return clip as Clip;\n\n  if (typeof clip === \"object\" && !(\"@type\" in clip)) {\n    return {\n      \"@type\": \"Clip\",\n      ...clip,\n    };\n  }\n\n  return clip as Clip;\n}\n\n/**\n * Processes seek action into SeekToAction schema type\n * @param action - SeekToAction with or without @type\n * @returns SeekToAction with @type\n */\nexport function processSeekToAction(\n  action: PotentialAction | Omit<PotentialAction, \"@type\">,\n): PotentialAction {\n  if (!action) return action as PotentialAction;\n\n  if (typeof action === \"object\" && !(\"@type\" in action)) {\n    return {\n      \"@type\": \"SeekToAction\",\n      ...action,\n    };\n  }\n\n  return action as PotentialAction;\n}\n\n/**\n * Processes instruction into string, HowToStep, or HowToSection schema type\n * @param instruction - String instruction or HowTo object\n * @returns String, HowToStep, or HowToSection with @type\n */\nexport function processInstruction(\n  instruction:\n    | string\n    | HowToStep\n    | HowToSection\n    | Omit<HowToStep, \"@type\">\n    | Omit<HowToSection, \"@type\">,\n): string | HowToStep | HowToSection {\n  if (isString(instruction)) {\n    return instruction;\n  }\n\n  if (hasType<HowToStep | HowToSection>(instruction)) {\n    // Process nested items if it's a section\n    if (\n      instruction[\"@type\"] === SCHEMA_TYPES.HOW_TO_SECTION &&\n      \"itemListElement\" in instruction\n    ) {\n      return {\n        ...instruction,\n        itemListElement: instruction.itemListElement.map((item) =>\n          processInstruction(item),\n        ),\n      } as HowToSection;\n    }\n    return instruction;\n  }\n\n  // Determine type based on properties\n  if (\"itemListElement\" in instruction) {\n    return {\n      \"@type\": SCHEMA_TYPES.HOW_TO_SECTION,\n      ...instruction,\n      itemListElement: instruction.itemListElement.map((item) =>\n        processInstruction(item),\n      ),\n    } as HowToSection;\n  }\n\n  return {\n    \"@type\": SCHEMA_TYPES.HOW_TO_STEP,\n    ...instruction,\n  } as HowToStep;\n}\n\n/**\n * Processes director into Person schema type\n * @param director - String name or Director object\n * @returns Person with @type\n */\nexport function processDirector(director: Director): Person {\n  return processSchemaType<Person>(\n    director,\n    SCHEMA_TYPES.PERSON,\n    (str) => ({ name: str }),\n    undefined,\n  );\n}\n\n// Dataset-specific processors\n\n/**\n * Processes creator(s) into Person or Organization schema type(s)\n * @param creator - Author or array of Authors\n * @returns Person/Organization or array of them with @type\n */\nexport function processCreator(\n  creator: Author | Author[],\n): Person | Organization | (Person | Organization)[] {\n  return Array.isArray(creator)\n    ? creator.map(processAuthor)\n    : processAuthor(creator);\n}\n\n/**\n * Processes identifier into string or PropertyValue schema type\n * @param identifier - String identifier or PropertyValue object\n * @returns String or PropertyValue with @type\n */\nexport function processIdentifier(\n  identifier: string | PropertyValue | Omit<PropertyValue, \"@type\">,\n): string | PropertyValue {\n  if (isString(identifier)) {\n    return identifier;\n  }\n\n  return processSchemaType<PropertyValue>(\n    identifier,\n    SCHEMA_TYPES.PROPERTY_VALUE,\n  );\n}\n\n/**\n * Processes spatial coverage into string or Place schema type\n * @param spatial - String location or DatasetPlace object\n * @returns String or DatasetPlace with @type and processed geo\n */\nexport function processSpatialCoverage(\n  spatial: string | DatasetPlace | Omit<DatasetPlace, \"@type\">,\n): string | DatasetPlace {\n  if (isString(spatial)) {\n    return spatial;\n  }\n\n  const processed: DatasetPlace = processSchemaType<DatasetPlace>(\n    spatial,\n    SCHEMA_TYPES.PLACE,\n  );\n\n  // Process nested geo if present\n  if (spatial.geo && typeof spatial.geo === \"object\" && !hasType(spatial.geo)) {\n    if (\"latitude\" in spatial.geo && \"longitude\" in spatial.geo) {\n      processed.geo = processSchemaType<GeoCoordinates>(\n        spatial.geo,\n        SCHEMA_TYPES.GEO_COORDINATES,\n      );\n    } else if (\n      \"box\" in spatial.geo ||\n      \"circle\" in spatial.geo ||\n      \"line\" in spatial.geo ||\n      \"polygon\" in spatial.geo\n    ) {\n      processed.geo = processSchemaType<GeoShape>(\n        spatial.geo,\n        SCHEMA_TYPES.GEO_SHAPE,\n      );\n    }\n  }\n\n  return processed;\n}\n\n/**\n * Processes data download into DataDownload schema type\n * @param download - DataDownload object with or without @type\n * @returns DataDownload with @type\n */\nexport function processDataDownload(\n  download: DataDownload | Omit<DataDownload, \"@type\">,\n): DataDownload {\n  return processSchemaType<DataDownload>(download, SCHEMA_TYPES.DATA_DOWNLOAD);\n}\n\n/**\n * Processes license into string URL or CreativeWork schema type\n * @param license - String URL or CreativeWork object\n * @returns String URL or CreativeWork with @type\n */\nexport function processLicense(\n  license: string | CreativeWork | Omit<CreativeWork, \"@type\">,\n): string | CreativeWork {\n  if (isString(license)) {\n    return license;\n  }\n\n  return processSchemaType<CreativeWork>(license, SCHEMA_TYPES.CREATIVE_WORK);\n}\n\n/**\n * Processes data catalog into DataCatalog schema type\n * @param catalog - DataCatalog object with or without @type\n * @returns DataCatalog with @type\n */\nexport function processDataCatalog(\n  catalog: DataCatalog | Omit<DataCatalog, \"@type\">,\n): DataCatalog {\n  return processSchemaType<DataCatalog>(catalog, SCHEMA_TYPES.DATA_CATALOG);\n}\n\n// JobPosting-specific processors\n\n/**\n * Processes hiring organization into Organization schema type\n * @param org - String name or Organization object\n * @returns Organization with @type and processed logo\n */\nexport function processHiringOrganization(\n  org: string | Organization | Omit<Organization, \"@type\">,\n): Organization {\n  if (isString(org)) {\n    return {\n      \"@type\": SCHEMA_TYPES.ORGANIZATION,\n      name: org,\n    };\n  }\n\n  const processed = processSchemaType<Organization>(\n    org,\n    SCHEMA_TYPES.ORGANIZATION,\n  );\n\n  // Process nested logo if present\n  if (processed.logo && !isString(processed.logo)) {\n    processed.logo = processImage(processed.logo);\n  }\n\n  return processed;\n}\n\n/**\n * Processes job location into Place schema type\n * @param location - String location or JobPlace object\n * @returns JobPlace with @type and processed address\n */\nexport function processJobLocation(\n  location: string | JobPlace | Omit<JobPlace, \"@type\">,\n): JobPlace {\n  if (isString(location)) {\n    return {\n      \"@type\": SCHEMA_TYPES.PLACE,\n      address: {\n        \"@type\": SCHEMA_TYPES.POSTAL_ADDRESS,\n        streetAddress: location,\n      },\n    };\n  }\n\n  const processed = processSchemaType<JobPlace>(location, SCHEMA_TYPES.PLACE);\n\n  // Process nested address\n  if (processed.address && !isString(processed.address)) {\n    processed.address = processAddress(processed.address);\n  }\n\n  return processed;\n}\n\n/**\n * Processes monetary amount into MonetaryAmount schema type\n * @param amount - MonetaryAmount object with or without @type\n * @returns MonetaryAmount with @type and processed value\n */\nexport function processMonetaryAmount(\n  amount: MonetaryAmount | Omit<MonetaryAmount, \"@type\">,\n): MonetaryAmount {\n  const processed = processSchemaType<MonetaryAmount>(amount, \"MonetaryAmount\");\n\n  // Process nested value as QuantitativeValue\n  processed.value = processSchemaType<QuantitativeValue>(\n    amount.value,\n    SCHEMA_TYPES.QUANTITATIVE_VALUE,\n  );\n\n  return processed;\n}\n\n/**\n * Processes rating into Rating schema type\n * @param rating - Rating object with or without @type\n * @returns Rating with @type\n */\nexport function processRating(rating: Rating | Omit<Rating, \"@type\">): Rating {\n  return processSchemaType<Rating>(rating, SCHEMA_TYPES.RATING);\n}\n\n/**\n * Processes job property value into PropertyValue schema type\n * @param identifier - String identifier or JobPropertyValue object\n * @returns JobPropertyValue with @type\n */\nexport function processJobPropertyValue(\n  identifier: string | JobPropertyValue | Omit<JobPropertyValue, \"@type\">,\n): JobPropertyValue {\n  return processSchemaType<JobPropertyValue>(\n    identifier,\n    SCHEMA_TYPES.PROPERTY_VALUE,\n    (str) => ({ value: str }),\n    undefined,\n  );\n}\n\n/**\n * Processes applicant location requirements into Country or State schema type\n * @param location - Location requirement object\n * @returns AdministrativeArea (Country or State) with @type\n */\nexport function processApplicantLocationRequirements(\n  location: Omit<Country, \"@type\"> | Omit<State, \"@type\"> | Country | State,\n): AdministrativeArea {\n  if (hasType<Country | State>(location)) {\n    return location;\n  }\n\n  // Improved detection logic\n  const name = location.name;\n  const statePatterns = [\n    /\\b[A-Z]{2}\\b/, // Two-letter state codes\n    /\\bstate\\b/i, // Contains \"state\"\n    /,/, // Contains comma (often \"City, State\")\n    /\\b(AL|AK|AZ|AR|CA|CO|CT|DE|FL|GA|HI|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY)\\b/, // US state codes\n  ];\n\n  const isState = statePatterns.some((pattern) => pattern.test(name));\n\n  return {\n    \"@type\": isState ? SCHEMA_TYPES.STATE : SCHEMA_TYPES.COUNTRY,\n    ...location,\n  } as AdministrativeArea;\n}\n\n/**\n * Processes education requirements into string or EducationalOccupationalCredential\n * @param education - String description or credential object\n * @returns String or EducationalOccupationalCredential with @type\n */\nexport function processEducationRequirements(\n  education:\n    | string\n    | EducationalOccupationalCredential\n    | Omit<EducationalOccupationalCredential, \"@type\">,\n): string | EducationalOccupationalCredential {\n  if (isString(education)) {\n    return education;\n  }\n\n  return processSchemaType<EducationalOccupationalCredential>(\n    education,\n    SCHEMA_TYPES.EDUCATIONAL_CREDENTIAL,\n  );\n}\n\n/**\n * Processes experience requirements into string or OccupationalExperienceRequirements\n * @param experience - String description or experience object\n * @returns String or OccupationalExperienceRequirements with @type\n */\nexport function processExperienceRequirements(\n  experience:\n    | string\n    | OccupationalExperienceRequirements\n    | Omit<OccupationalExperienceRequirements, \"@type\">,\n): string | OccupationalExperienceRequirements {\n  if (isString(experience)) {\n    return experience;\n  }\n\n  return processSchemaType<OccupationalExperienceRequirements>(\n    experience,\n    SCHEMA_TYPES.OCCUPATIONAL_EXPERIENCE,\n  );\n}\n\n// DiscussionForumPosting-specific processors\n\n/**\n * Processes interaction statistic into InteractionCounter schema type\n * @param statistic - InteractionCounter object with or without @type\n * @returns InteractionCounter with @type\n */\nexport function processInteractionStatistic(\n  statistic: InteractionCounter | Omit<InteractionCounter, \"@type\">,\n): InteractionCounter {\n  return processSchemaType<InteractionCounter>(\n    statistic,\n    SCHEMA_TYPES.INTERACTION_COUNTER,\n  );\n}\n\n/**\n * Processes shared content into WebPage, ImageObject, or VideoObject schema type\n * @param content - SharedContent (string URL or object)\n * @returns ForumWebPage, ImageObject, or VideoObject with @type\n */\nexport function processSharedContent(\n  content: SharedContent,\n): ForumWebPage | ImageObject | VideoObject {\n  if (isString(content)) {\n    return {\n      \"@type\": SCHEMA_TYPES.WEB_PAGE,\n      url: content,\n    };\n  }\n\n  if (hasType<ForumWebPage | ImageObject | VideoObject>(content)) {\n    return content;\n  }\n\n  // Improved type detection\n  const hasVideoProperties =\n    \"uploadDate\" in content && \"thumbnailUrl\" in content;\n  const hasImageProperties =\n    \"url\" in content && (\"width\" in content || \"height\" in content);\n\n  if (hasVideoProperties) {\n    return processSchemaType<VideoObject>(content, SCHEMA_TYPES.VIDEO_OBJECT);\n  }\n\n  if (hasImageProperties) {\n    return processSchemaType<ImageObject>(content, SCHEMA_TYPES.IMAGE_OBJECT);\n  }\n\n  // Default to WebPage\n  return processSchemaType<ForumWebPage>(content, SCHEMA_TYPES.WEB_PAGE);\n}\n\n/**\n * Processes comment into Comment schema type with nested fields\n * @param comment - Comment object with or without @type\n * @returns Comment with @type and processed nested fields\n */\nexport function processComment(\n  comment: Comment | Omit<Comment, \"@type\">,\n): Comment {\n  const processed: Comment = processSchemaType<Comment>(\n    comment,\n    SCHEMA_TYPES.COMMENT,\n  );\n\n  // Process nested fields\n  if (comment.author) {\n    processed.author = processAuthor(comment.author);\n  }\n\n  if (comment.image && !isString(comment.image)) {\n    processed.image = processImage(comment.image);\n  }\n\n  if (comment.video) {\n    processed.video = processVideo(comment.video);\n  }\n\n  if (comment.interactionStatistic) {\n    processed.interactionStatistic = Array.isArray(comment.interactionStatistic)\n      ? comment.interactionStatistic.map(processInteractionStatistic)\n      : processInteractionStatistic(comment.interactionStatistic);\n  }\n\n  if (comment.sharedContent) {\n    processed.sharedContent = processSharedContent(comment.sharedContent);\n  }\n\n  // Process nested comments recursively\n  if (comment.comment) {\n    processed.comment = comment.comment.map(processComment);\n  }\n\n  return processed;\n}\n\n/**\n * Processes isPartOf into string URL or CreativeWork schema type\n * @param isPartOf - String URL or ForumCreativeWork object\n * @returns String URL or ForumCreativeWork with @type\n */\nexport function processIsPartOf(\n  isPartOf: string | ForumCreativeWork | Omit<ForumCreativeWork, \"@type\">,\n): string | ForumCreativeWork {\n  if (isString(isPartOf)) {\n    return isPartOf;\n  }\n\n  return processSchemaType<ForumCreativeWork>(\n    isPartOf,\n    SCHEMA_TYPES.CREATIVE_WORK,\n  );\n}\n\n// VacationRental-specific processors\n\n/**\n * Processes brand into Brand or Organization schema type\n * @param brand - Brand or Organization object with or without @type\n * @returns Brand or Organization with @type\n */\nexport function processBrand(\n  brand:\n    | Brand\n    | Organization\n    | Omit<Brand, \"@type\">\n    | Omit<Organization, \"@type\">,\n): Brand | Organization {\n  // If it already has a type, return as-is\n  if (\"@type\" in brand) {\n    return brand as Brand | Organization;\n  }\n\n  // Check if it has Organization-specific properties\n  if (\"logo\" in brand || \"address\" in brand || \"contactPoint\" in brand) {\n    const org: Organization = {\n      \"@type\": SCHEMA_TYPES.ORGANIZATION,\n      ...brand,\n    };\n    processOrganizationFields(org);\n    return org;\n  }\n\n  // Default to Brand\n  return processSchemaType<Brand>(brand, SCHEMA_TYPES.BRAND);\n}\n\n/**\n * Processes bed details into BedDetails schema type\n * @param bed - BedDetails object with or without @type\n * @returns BedDetails with @type\n */\nexport function processBedDetails(\n  bed: BedDetails | Omit<BedDetails, \"@type\">,\n): BedDetails {\n  return processSchemaType<BedDetails>(bed, SCHEMA_TYPES.BED_DETAILS);\n}\n\n/**\n * Processes location feature into LocationFeatureSpecification schema type\n * @param feature - LocationFeatureSpecification object with or without @type\n * @returns LocationFeatureSpecification with @type\n */\nexport function processLocationFeatureSpecification(\n  feature:\n    | LocationFeatureSpecification\n    | Omit<LocationFeatureSpecification, \"@type\">,\n): LocationFeatureSpecification {\n  return processSchemaType<LocationFeatureSpecification>(\n    feature,\n    SCHEMA_TYPES.LOCATION_FEATURE,\n  );\n}\n\n/**\n * Processes accommodation into Accommodation schema type with nested fields\n * @param accommodation - Accommodation object with or without @type\n * @returns Accommodation with @type and processed nested fields\n */\nexport function processAccommodation(\n  accommodation: Accommodation | Omit<Accommodation, \"@type\">,\n): Accommodation {\n  const processed: Accommodation = processSchemaType<Accommodation>(\n    accommodation,\n    SCHEMA_TYPES.ACCOMMODATION,\n  );\n\n  // Process nested bed details\n  if (accommodation.bed) {\n    processed.bed = Array.isArray(accommodation.bed)\n      ? accommodation.bed.map(processBedDetails)\n      : processBedDetails(accommodation.bed);\n  }\n\n  // Process occupancy\n  if (accommodation.occupancy) {\n    processed.occupancy = processNumberOfEmployees(\n      accommodation.occupancy,\n    ) as QuantitativeValue;\n  }\n\n  // Process amenity features\n  if (accommodation.amenityFeature) {\n    processed.amenityFeature = Array.isArray(accommodation.amenityFeature)\n      ? accommodation.amenityFeature.map(processLocationFeatureSpecification)\n      : processLocationFeatureSpecification(accommodation.amenityFeature);\n  }\n\n  // Process floor size\n  if (accommodation.floorSize) {\n    processed.floorSize = processNumberOfEmployees(\n      accommodation.floorSize,\n    ) as QuantitativeValue;\n  }\n\n  return processed;\n}\n\n// Course-specific processors\n\n/**\n * Processes provider into Organization schema type\n * @param provider - String name or Provider object\n * @returns Organization with @type\n */\nexport function processProvider(provider: Provider): Organization {\n  return processSchemaType<Organization>(\n    provider,\n    SCHEMA_TYPES.ORGANIZATION,\n    (str) => ({ name: str }),\n    undefined,\n  );\n}\n\n/**\n * Processes funder(s) into Person or Organization schema type(s)\n * @param funder - Author or array of Authors representing funders\n * @returns Person/Organization or array of them with @type\n */\nexport function processFunder(\n  funder: Author | Author[],\n): Person | Organization | (Person | Organization)[] {\n  if (Array.isArray(funder)) {\n    return funder.map(processFunderSingle);\n  }\n  return processFunderSingle(funder);\n}\n\n/**\n * Helper to process a single funder\n * @param funder - Author representing a funder\n * @returns Person or Organization with @type (defaults to Organization)\n */\nfunction processFunderSingle(funder: Author): Person | Organization {\n  if (isString(funder)) {\n    return {\n      \"@type\": SCHEMA_TYPES.ORGANIZATION,\n      name: funder,\n    };\n  }\n\n  if (hasType<Person | Organization>(funder)) {\n    return funder;\n  }\n\n  // For funders without @type, default to Organization\n  // (funding bodies are typically organizations)\n  return {\n    \"@type\": SCHEMA_TYPES.ORGANIZATION,\n    ...funder,\n  } as Organization;\n}\n\n// SoftwareApplication-specific processors\n\n/**\n * Processes screenshot the same way as images\n * @param screenshot - URL string or ImageObject\n * @returns URL string or ImageObject with @type\n */\nexport function processScreenshot(\n  screenshot: string | ImageObject | Omit<ImageObject, \"@type\">,\n): string | ImageObject {\n  return processImage(screenshot);\n}\n\n/**\n * Processes feature list - no transformation needed\n * @param features - String or array of strings\n * @returns String or array of strings as-is\n */\nexport function processFeatureList(\n  features: string | string[],\n): string | string[] {\n  return features;\n}\n\n// ClaimReview-specific processors\n\n/**\n * Processes claim review rating into ClaimReviewRating schema type\n * @param rating - Rating object with or without @type\n * @returns ClaimReviewRating with @type\n */\nexport function processClaimReviewRating(\n  rating: ClaimReviewRating | Omit<ClaimReviewRating, \"@type\">,\n): ClaimReviewRating {\n  return processSchemaType<ClaimReviewRating>(rating, SCHEMA_TYPES.RATING);\n}\n\n/**\n * Processes claim into Claim schema type with nested fields\n * @param claim - Claim object with or without @type\n * @returns Claim with @type and processed nested fields\n */\nexport function processClaim(claim: Claim | Omit<Claim, \"@type\">): Claim {\n  const processed: Claim = processSchemaType<Claim>(claim, SCHEMA_TYPES.CLAIM);\n\n  // Process nested author\n  if (claim.author) {\n    processed.author = processAuthor(claim.author);\n  }\n\n  // Process appearance(s)\n  if (claim.appearance) {\n    if (Array.isArray(claim.appearance)) {\n      processed.appearance = claim.appearance.map(processAppearance);\n    } else {\n      processed.appearance = processAppearance(claim.appearance);\n    }\n  }\n\n  // Process firstAppearance\n  if (claim.firstAppearance) {\n    processed.firstAppearance = processAppearance(claim.firstAppearance);\n  }\n\n  return processed;\n}\n\n/**\n * Processes appearance into string URL or ClaimCreativeWork schema type\n * @param appearance - String URL or CreativeWork object\n * @returns String URL or ClaimCreativeWork with @type\n */\nexport function processAppearance(\n  appearance: string | ClaimCreativeWork | Omit<ClaimCreativeWork, \"@type\">,\n): string | ClaimCreativeWork {\n  if (isString(appearance)) {\n    return appearance;\n  }\n\n  const processed = processSchemaType<ClaimCreativeWork>(\n    appearance,\n    SCHEMA_TYPES.CREATIVE_WORK,\n  );\n\n  // Process nested fields\n  if (appearance.author) {\n    processed.author = processAuthor(appearance.author);\n  }\n\n  if (appearance.publisher && !isString(appearance.publisher)) {\n    processed.publisher = processPublisher(appearance.publisher);\n  }\n\n  return processed;\n}\n\n/**\n * Processes WebPageElement for marking paywalled sections\n * @param element - WebPageElement object with or without @type\n * @returns WebPageElement with @type\n * @example\n * processWebPageElement({ isAccessibleForFree: false, cssSelector: \".paywall\" })\n * // { \"@type\": \"WebPageElement\", isAccessibleForFree: false, cssSelector: \".paywall\" }\n */\nexport function processWebPageElement(\n  element: WebPageElement | Omit<WebPageElement, \"@type\">,\n): WebPageElement {\n  return processSchemaType<WebPageElement>(\n    element,\n    SCHEMA_TYPES.WEB_PAGE_ELEMENT,\n  );\n}\n\n// Product-specific processors\n\n/**\n * Processes product offer into ProductOffer schema type\n * @param offer - ProductOffer object with or without @type\n * @returns ProductOffer with @type and processed nested fields\n */\nexport function processProductOffer(\n  offer: ProductOffer | Omit<ProductOffer, \"@type\">,\n): ProductOffer {\n  const processed: ProductOffer = processSchemaType<ProductOffer>(\n    offer,\n    SCHEMA_TYPES.OFFER,\n  );\n\n  // Process nested seller if present\n  if (offer.seller) {\n    processed.seller = processAuthor(offer.seller);\n  }\n\n  // Process nested priceSpecification if present\n  if (offer.priceSpecification) {\n    if (Array.isArray(offer.priceSpecification)) {\n      processed.priceSpecification = offer.priceSpecification.map(\n        processPriceSpecification,\n      );\n    } else {\n      processed.priceSpecification = processPriceSpecification(\n        offer.priceSpecification,\n      );\n    }\n  }\n\n  // Process nested hasMerchantReturnPolicy if present\n  if (offer.hasMerchantReturnPolicy) {\n    if (Array.isArray(offer.hasMerchantReturnPolicy)) {\n      processed.hasMerchantReturnPolicy = offer.hasMerchantReturnPolicy.map(\n        processMerchantReturnPolicy,\n      );\n    } else {\n      processed.hasMerchantReturnPolicy = processMerchantReturnPolicy(\n        offer.hasMerchantReturnPolicy,\n      );\n    }\n  }\n\n  // Process nested shippingDetails if present\n  if (offer.shippingDetails) {\n    if (Array.isArray(offer.shippingDetails)) {\n      processed.shippingDetails = offer.shippingDetails.map(\n        processOfferShippingDetails,\n      );\n    } else {\n      processed.shippingDetails = processOfferShippingDetails(\n        offer.shippingDetails,\n      );\n    }\n  }\n\n  return processed;\n}\n\n/**\n * Processes aggregate offer into AggregateOffer schema type\n * @param offer - AggregateOffer object with or without @type\n * @returns AggregateOffer with @type and processed nested offers\n */\nexport function processAggregateOffer(\n  offer: AggregateOffer | Omit<AggregateOffer, \"@type\">,\n): AggregateOffer {\n  const processed: AggregateOffer = processSchemaType<AggregateOffer>(\n    offer,\n    SCHEMA_TYPES.AGGREGATE_OFFER,\n  );\n\n  // Process nested offers if present\n  if (offer.offers) {\n    processed.offers = offer.offers.map(processProductOffer);\n  }\n\n  return processed;\n}\n\n/**\n * Processes price specification into PriceSpecification or UnitPriceSpecification schema type\n * @param spec - PriceSpecification or UnitPriceSpecification object with or without @type\n * @returns PriceSpecification or UnitPriceSpecification with @type\n */\nexport function processPriceSpecification(\n  spec:\n    | PriceSpecification\n    | UnitPriceSpecification\n    | Omit<PriceSpecification, \"@type\">\n    | Omit<UnitPriceSpecification, \"@type\">,\n): PriceSpecification | UnitPriceSpecification {\n  // Check if it's a UnitPriceSpecification\n  if (\n    \"priceType\" in spec ||\n    \"validForMemberTier\" in spec ||\n    \"membershipPointsEarned\" in spec ||\n    \"referenceQuantity\" in spec\n  ) {\n    return processUnitPriceSpecification(\n      spec as UnitPriceSpecification | Omit<UnitPriceSpecification, \"@type\">,\n    );\n  }\n  return processSchemaType<PriceSpecification>(\n    spec as PriceSpecification | Omit<PriceSpecification, \"@type\">,\n    SCHEMA_TYPES.PRICE_SPECIFICATION,\n  );\n}\n\n/**\n * Processes unit price specification into UnitPriceSpecification schema type\n * @param spec - UnitPriceSpecification object with or without @type\n * @returns UnitPriceSpecification with @type\n */\nexport function processUnitPriceSpecification(\n  spec: UnitPriceSpecification | Omit<UnitPriceSpecification, \"@type\">,\n): UnitPriceSpecification {\n  const processed = processSchemaType<UnitPriceSpecification>(\n    spec,\n    SCHEMA_TYPES.UNIT_PRICE_SPECIFICATION,\n  );\n\n  // Process nested validForMemberTier if present\n  if (spec.validForMemberTier) {\n    if (Array.isArray(spec.validForMemberTier)) {\n      processed.validForMemberTier = spec.validForMemberTier.map(\n        processMemberProgramTier,\n      );\n    } else {\n      processed.validForMemberTier = processMemberProgramTier(\n        spec.validForMemberTier,\n      );\n    }\n  }\n\n  // Process nested referenceQuantity if present\n  if (spec.referenceQuantity) {\n    processed.referenceQuantity = processQuantitativeValue(\n      spec.referenceQuantity,\n    );\n  }\n\n  return processed;\n}\n\n/**\n * Processes QuantitativeValue into schema type\n * @param value - QuantitativeValue object with or without @type\n * @returns QuantitativeValue with @type\n */\nexport function processQuantitativeValue(\n  value: QuantitativeValue | Omit<QuantitativeValue, \"@type\">,\n): QuantitativeValue {\n  const processed = processSchemaType<QuantitativeValue>(\n    value,\n    SCHEMA_TYPES.QUANTITATIVE_VALUE,\n  );\n\n  // Process nested valueReference if present\n  if (value.valueReference) {\n    processed.valueReference = processQuantitativeValue(value.valueReference);\n  }\n\n  return processed;\n}\n\n/**\n * Processes product item list into ProductItemList schema type\n * @param list - ProductItemList object with or without @type\n * @returns ProductItemList with @type and processed items\n */\nexport function processProductItemList(\n  list: ProductItemList | Omit<ProductItemList, \"@type\">,\n): ProductItemList {\n  const processed: ProductItemList = processSchemaType<ProductItemList>(\n    list,\n    SCHEMA_TYPES.ITEM_LIST,\n  );\n\n  // Process nested list items\n  if (list.itemListElement) {\n    processed.itemListElement = list.itemListElement.map((item, index) => {\n      const processedItem = processSchemaType<ProductListItem>(\n        item,\n        SCHEMA_TYPES.LIST_ITEM,\n      );\n      // Ensure position is set if not provided\n      if (!processedItem.position) {\n        processedItem.position = index + 1;\n      }\n      return processedItem;\n    });\n  }\n\n  return processed;\n}\n\n/**\n * Processes product review into ProductReview schema type with pros/cons\n * @param review - ProductReview object with or without @type\n * @returns ProductReview with @type and processed nested fields\n */\nexport function processProductReview(\n  review: ProductReview | Omit<ProductReview, \"@type\">,\n): ProductReview {\n  const processed: ProductReview = processSchemaType<ProductReview>(\n    review,\n    SCHEMA_TYPES.REVIEW,\n  );\n\n  // Process nested rating\n  if (review.reviewRating) {\n    processed.reviewRating = processSchemaType<Rating>(\n      review.reviewRating,\n      SCHEMA_TYPES.RATING,\n    );\n  }\n\n  // Process nested author\n  if (review.author) {\n    processed.author = processAuthor(review.author);\n  }\n\n  // Process positive notes (pros)\n  if (review.positiveNotes) {\n    processed.positiveNotes = processProductItemList(review.positiveNotes);\n  }\n\n  // Process negative notes (cons)\n  if (review.negativeNotes) {\n    processed.negativeNotes = processProductItemList(review.negativeNotes);\n  }\n\n  return processed;\n}\n\n/**\n * Processes variesBy property to ensure full schema.org URLs\n * @param variesBy - Simple string or full URL, single or array\n * @returns Processed variesBy with full schema.org URLs\n */\nexport function processVariesBy(\n  variesBy: VariesBy | VariesBy[],\n): string | string[] {\n  const processOne = (value: VariesBy): string => {\n    // If it's already a full URL, return as-is\n    if (value.startsWith(\"https://schema.org/\")) {\n      return value;\n    }\n    // Otherwise, prepend the schema.org URL\n    return `https://schema.org/${value}`;\n  };\n\n  if (Array.isArray(variesBy)) {\n    return variesBy.map(processOne);\n  }\n  return processOne(variesBy);\n}\n\n/**\n * Processes a product variant for use in ProductGroup\n * @param variant - Product object, simplified variant with just URL, or processed Product\n * @returns Product with @type or simplified variant object\n */\nexport function processProductVariant(\n  variant:\n    | Product\n    | Omit<Product, \"@type\">\n    | { url: string }\n    | { \"@type\": \"Product\"; url: string },\n): Product | { \"@type\": \"Product\"; url: string } | { url: string } {\n  // If it's just a URL reference, return as-is\n  if (\"url\" in variant && Object.keys(variant).length <= 2) {\n    // It's a simple URL reference (with or without @type)\n    return variant as { url: string } | { \"@type\": \"Product\"; url: string };\n  }\n\n  // It's a full Product variant\n  const product = variant as Product | Omit<Product, \"@type\">;\n\n  if (\"@type\" in product) {\n    return product as Product;\n  }\n\n  const processed: Product = {\n    \"@type\": SCHEMA_TYPES.PRODUCT,\n    ...product,\n  };\n\n  // Process nested fields\n  if (product.image) {\n    processed.image = Array.isArray(product.image)\n      ? product.image.map(processImage)\n      : processImage(product.image);\n  }\n\n  if (product.brand) {\n    if (typeof product.brand === \"string\") {\n      processed.brand = {\n        \"@type\": SCHEMA_TYPES.BRAND,\n        name: product.brand,\n      };\n    } else {\n      processed.brand = processBrand(product.brand);\n    }\n  }\n\n  if (product.offers) {\n    if (Array.isArray(product.offers)) {\n      processed.offers = product.offers.map((offer) => {\n        if (\"lowPrice\" in offer && \"priceCurrency\" in offer) {\n          return processAggregateOffer(\n            offer as Parameters<typeof processAggregateOffer>[0],\n          );\n        }\n        return processProductOffer(\n          offer as Parameters<typeof processProductOffer>[0],\n        );\n      });\n    } else if (\n      \"lowPrice\" in product.offers &&\n      \"priceCurrency\" in product.offers\n    ) {\n      processed.offers = processAggregateOffer(\n        product.offers as Parameters<typeof processAggregateOffer>[0],\n      );\n    } else {\n      processed.offers = processProductOffer(\n        product.offers as Parameters<typeof processProductOffer>[0],\n      );\n    }\n  }\n\n  if (product.review) {\n    processed.review = Array.isArray(product.review)\n      ? product.review.map(processProductReview)\n      : processProductReview(product.review);\n  }\n\n  if (product.aggregateRating) {\n    processed.aggregateRating = processAggregateRating(product.aggregateRating);\n  }\n\n  if (product.manufacturer) {\n    processed.manufacturer = processAuthor(product.manufacturer);\n  }\n\n  // Process weight/dimensions - add @type if missing\n  if (\n    product.weight &&\n    typeof product.weight === \"object\" &&\n    !(\"@type\" in product.weight)\n  ) {\n    processed.weight = { \"@type\": \"QuantitativeValue\", ...product.weight };\n  }\n\n  if (\n    product.width &&\n    typeof product.width === \"object\" &&\n    !(\"@type\" in product.width)\n  ) {\n    processed.width = { \"@type\": \"QuantitativeValue\", ...product.width };\n  }\n\n  if (\n    product.height &&\n    typeof product.height === \"object\" &&\n    !(\"@type\" in product.height)\n  ) {\n    processed.height = { \"@type\": \"QuantitativeValue\", ...product.height };\n  }\n\n  if (\n    product.depth &&\n    typeof product.depth === \"object\" &&\n    !(\"@type\" in product.depth)\n  ) {\n    processed.depth = { \"@type\": \"QuantitativeValue\", ...product.depth };\n  }\n\n  return processed;\n}\n\n/**\n * Processes certification into Certification schema type\n * @param cert - Certification object with or without @type\n * @returns Certification with @type\n */\nexport function processCertification(\n  cert: Certification | Omit<Certification, \"@type\">,\n): Certification {\n  const processed = processSchemaType<Certification>(\n    cert,\n    SCHEMA_TYPES.CERTIFICATION,\n  );\n\n  // Process nested issuedBy as Organization\n  if (cert.issuedBy) {\n    if (typeof cert.issuedBy === \"object\" && !(\"@type\" in cert.issuedBy)) {\n      processed.issuedBy = {\n        \"@type\": SCHEMA_TYPES.ORGANIZATION,\n        ...cert.issuedBy,\n      };\n    } else {\n      processed.issuedBy = cert.issuedBy;\n    }\n  }\n\n  // Process nested certificationRating if present\n  if (cert.certificationRating) {\n    processed.certificationRating = processRating(cert.certificationRating);\n  }\n\n  return processed;\n}\n\n/**\n * Processes people audience into PeopleAudience schema type\n * @param audience - PeopleAudience object with or without @type\n * @returns PeopleAudience with @type\n */\nexport function processPeopleAudience(\n  audience: PeopleAudience | Omit<PeopleAudience, \"@type\">,\n): PeopleAudience {\n  const processed = processSchemaType<PeopleAudience>(\n    audience,\n    SCHEMA_TYPES.PEOPLE_AUDIENCE,\n  );\n\n  // Process nested suggestedAge if present\n  if (audience.suggestedAge) {\n    processed.suggestedAge = processQuantitativeValue(audience.suggestedAge);\n  }\n\n  return processed;\n}\n\n/**\n * Processes size specification into SizeSpecification schema type\n * @param size - String or SizeSpecification object with or without @type\n * @returns SizeSpecification with @type or string\n */\nexport function processSizeSpecification(\n  size: string | SizeSpecification | Omit<SizeSpecification, \"@type\">,\n): string | SizeSpecification {\n  if (typeof size === \"string\") {\n    return size;\n  }\n  return processSchemaType<SizeSpecification>(\n    size,\n    SCHEMA_TYPES.SIZE_SPECIFICATION,\n  );\n}\n\n/**\n * Processes 3D model into ThreeDModel schema type\n * @param model - ThreeDModel object with or without @type\n * @returns ThreeDModel with @type\n */\nexport function processThreeDModel(\n  model: ThreeDModel | Omit<ThreeDModel, \"@type\">,\n): ThreeDModel {\n  const processed = processSchemaType<ThreeDModel>(\n    model,\n    SCHEMA_TYPES.THREE_D_MODEL,\n  );\n\n  // Process nested encoding if present\n  if (model.encoding && !(\"@type\" in model.encoding)) {\n    processed.encoding = {\n      \"@type\": \"MediaObject\",\n      ...model.encoding,\n    };\n  }\n\n  return processed;\n}\n\n/**\n * Processes defined region into DefinedRegion schema type\n * @param region - DefinedRegion object with or without @type\n * @returns DefinedRegion with @type\n */\nexport function processDefinedRegion(\n  region: DefinedRegion | Omit<DefinedRegion, \"@type\">,\n): DefinedRegion {\n  return processSchemaType<DefinedRegion>(region, SCHEMA_TYPES.DEFINED_REGION);\n}\n\n/**\n * Processes shipping delivery time into ShippingDeliveryTime schema type\n * @param time - ShippingDeliveryTime object with or without @type\n * @returns ShippingDeliveryTime with @type\n */\nexport function processShippingDeliveryTime(\n  time: ShippingDeliveryTime | Omit<ShippingDeliveryTime, \"@type\">,\n): ShippingDeliveryTime {\n  const processed = processSchemaType<ShippingDeliveryTime>(\n    time,\n    SCHEMA_TYPES.SHIPPING_DELIVERY_TIME,\n  );\n\n  // Process nested handlingTime if present\n  if (time.handlingTime) {\n    processed.handlingTime = processQuantitativeValue(time.handlingTime);\n  }\n\n  // Process nested transitTime if present\n  if (time.transitTime) {\n    processed.transitTime = processQuantitativeValue(time.transitTime);\n  }\n\n  return processed;\n}\n\n/**\n * Processes offer shipping details into OfferShippingDetails schema type\n * @param details - OfferShippingDetails object with or without @type\n * @returns OfferShippingDetails with @type\n */\nexport function processOfferShippingDetails(\n  details: OfferShippingDetails | Omit<OfferShippingDetails, \"@type\">,\n): OfferShippingDetails {\n  const processed = processSchemaType<OfferShippingDetails>(\n    details,\n    SCHEMA_TYPES.OFFER_SHIPPING_DETAILS,\n  );\n\n  // Process nested shippingRate if present\n  if (details.shippingRate) {\n    processed.shippingRate = processSimpleMonetaryAmount(details.shippingRate);\n  }\n\n  // Process nested shippingDestination if present\n  if (details.shippingDestination) {\n    if (Array.isArray(details.shippingDestination)) {\n      processed.shippingDestination =\n        details.shippingDestination.map(processDefinedRegion);\n    } else {\n      processed.shippingDestination = processDefinedRegion(\n        details.shippingDestination,\n      );\n    }\n  }\n\n  // Process nested deliveryTime if present\n  if (details.deliveryTime) {\n    processed.deliveryTime = processShippingDeliveryTime(details.deliveryTime);\n  }\n\n  return processed;\n}\n\n// Review/AggregateRating-specific processors\n\nexport type ItemReviewedType =\n  | \"Book\"\n  | \"Course\"\n  | \"CreativeWorkSeason\"\n  | \"CreativeWorkSeries\"\n  | \"Episode\"\n  | \"Event\"\n  | \"Game\"\n  | \"HowTo\"\n  | \"LocalBusiness\"\n  | \"MediaObject\"\n  | \"Movie\"\n  | \"MusicPlaylist\"\n  | \"MusicRecording\"\n  | \"Organization\"\n  | \"Product\"\n  | \"Recipe\"\n  | \"SoftwareApplication\";\n\ntype ItemReviewedInput =\n  | string\n  | ({ name: string; \"@type\"?: ItemReviewedType } & Record<string, unknown>);\n\n/**\n * Processes itemReviewed into a specific schema type if possible, otherwise Thing\n * @param itemReviewed - String name or object with optional @type\n * @param defaultType - Optional default @type to apply when missing\n */\nexport function processItemReviewed(\n  itemReviewed: ItemReviewedInput,\n  defaultType?: ItemReviewedType | \"Thing\",\n): Record<string, unknown> {\n  if (isString(itemReviewed)) {\n    return { \"@type\": defaultType || \"Thing\", name: itemReviewed };\n  }\n\n  if (\n    typeof itemReviewed === \"object\" &&\n    itemReviewed !== null &&\n    \"@type\" in itemReviewed\n  ) {\n    return itemReviewed as Record<string, unknown>;\n  }\n\n  // Try light heuristics if no @type present\n  const candidate: Record<string, unknown> = {\n    ...(itemReviewed as Record<string, unknown>),\n  };\n  const inferType = (): ItemReviewedType | \"Thing\" => {\n    if (defaultType) return defaultType;\n    if (\"brand\" in candidate || \"sku\" in candidate) return \"Product\";\n    if (\"recipeIngredient\" in candidate || \"recipeInstructions\" in candidate)\n      return \"Recipe\";\n    if (\"servesCuisine\" in candidate || \"address\" in candidate)\n      return \"LocalBusiness\";\n    if (\"applicationCategory\" in candidate || \"operatingSystem\" in candidate)\n      return \"SoftwareApplication\";\n    if (\"director\" in candidate || \"actor\" in candidate) return \"Movie\";\n    if (\"provider\" in candidate) return \"Course\";\n    return \"Thing\";\n  };\n\n  return { \"@type\": inferType(), ...candidate };\n}\n\n// HowTo-specific processors\n\n/**\n * Processes HowTo direction into HowToDirection schema type\n * @param direction - HowToDirection object with or without @type\n * @returns HowToDirection with @type\n */\nexport function processHowToDirection(\n  direction: HowToDirection | Omit<HowToDirection, \"@type\">,\n): HowToDirection {\n  const processed = processSchemaType<HowToDirection>(\n    direction,\n    SCHEMA_TYPES.HOW_TO_DIRECTION,\n  );\n\n  // Process nested media if present\n  if (direction.beforeMedia && !isString(direction.beforeMedia)) {\n    processed.beforeMedia = processImage(direction.beforeMedia);\n  }\n  if (direction.afterMedia && !isString(direction.afterMedia)) {\n    processed.afterMedia = processImage(direction.afterMedia);\n  }\n  if (direction.duringMedia && !isString(direction.duringMedia)) {\n    processed.duringMedia = processImage(direction.duringMedia);\n  }\n\n  return processed;\n}\n\n/**\n * Processes HowTo tip into HowToTip schema type\n * @param tip - HowToTip object with or without @type\n * @returns HowToTip with @type\n */\nexport function processHowToTip(\n  tip: HowToTip | Omit<HowToTip, \"@type\">,\n): HowToTip {\n  return processSchemaType<HowToTip>(tip, SCHEMA_TYPES.HOW_TO_TIP);\n}\n\n/**\n * Processes HowTo step item (direction or tip) into appropriate schema type\n * @param item - HowToDirection or HowToTip with or without @type\n * @returns Processed item with @type\n */\nfunction processHowToStepItem(\n  item:\n    | HowToDirection\n    | HowToTip\n    | Omit<HowToDirection, \"@type\">\n    | Omit<HowToTip, \"@type\">,\n): HowToDirection | HowToTip {\n  if (hasType<HowToDirection | HowToTip>(item)) {\n    if (item[\"@type\"] === SCHEMA_TYPES.HOW_TO_DIRECTION) {\n      return processHowToDirection(item as HowToDirection);\n    }\n    return processHowToTip(item as HowToTip);\n  }\n\n  // Infer type based on properties - tips typically only have text\n  // Directions may have beforeMedia, afterMedia, duringMedia\n  if (\"beforeMedia\" in item || \"afterMedia\" in item || \"duringMedia\" in item) {\n    return processHowToDirection(item as Omit<HowToDirection, \"@type\">);\n  }\n\n  // Default to direction for items with just text (more common)\n  return processHowToDirection(item as Omit<HowToDirection, \"@type\">);\n}\n\n/**\n * Processes HowTo step into HowToStep schema type\n * @param step - String, HowToStep object with or without @type\n * @returns HowToStep with @type\n */\nexport function processHowToStep(\n  step: string | HowToStepType | Omit<HowToStepType, \"@type\">,\n): string | HowToStepType {\n  if (isString(step)) {\n    return step;\n  }\n\n  const processed = processSchemaType<HowToStepType>(\n    step,\n    SCHEMA_TYPES.HOW_TO_STEP,\n  );\n\n  // Process nested image if present\n  if (step.image && !isString(step.image)) {\n    processed.image = processImage(step.image);\n  }\n\n  // Process nested itemListElement (directions/tips) if present\n  if (step.itemListElement) {\n    processed.itemListElement = step.itemListElement.map(processHowToStepItem);\n  }\n\n  return processed;\n}\n\n/**\n * Processes HowTo section into HowToSection schema type\n * @param section - HowToSection object with or without @type\n * @returns HowToSection with @type\n */\nexport function processHowToSection(\n  section: HowToSectionType | Omit<HowToSectionType, \"@type\">,\n): HowToSectionType {\n  const processed = processSchemaType<HowToSectionType>(\n    section,\n    SCHEMA_TYPES.HOW_TO_SECTION,\n  );\n\n  // Process nested steps\n  if (section.itemListElement) {\n    processed.itemListElement = section.itemListElement.map((item) => {\n      const result = processHowToStep(item);\n      return typeof result === \"string\"\n        ? ({ \"@type\": \"HowToStep\", text: result } as HowToStepType)\n        : result;\n    });\n  }\n\n  return processed;\n}\n\n/**\n * Processes HowTo step (can be string, HowToStep, or HowToSection)\n * @param step - String, HowToStep, or HowToSection with or without @type\n * @returns Processed step with @type\n */\nexport function processStep(\n  step: Step,\n): string | HowToStepType | HowToSectionType {\n  if (isString(step)) {\n    return step;\n  }\n\n  if (hasType<HowToStepType | HowToSectionType>(step)) {\n    if (step[\"@type\"] === SCHEMA_TYPES.HOW_TO_SECTION) {\n      return processHowToSection(step as HowToSectionType);\n    }\n    const result = processHowToStep(step as HowToStepType);\n    return typeof result === \"string\"\n      ? ({ \"@type\": \"HowToStep\", text: result } as HowToStepType)\n      : result;\n  }\n\n  // Infer type based on properties\n  if (\"itemListElement\" in step && \"name\" in step) {\n    // Sections have itemListElement and a name\n    return processHowToSection(step as Omit<HowToSectionType, \"@type\">);\n  }\n\n  // Default to step\n  const result = processHowToStep(step as Omit<HowToStepType, \"@type\">);\n  return typeof result === \"string\"\n    ? ({ \"@type\": \"HowToStep\", text: result } as HowToStepType)\n    : result;\n}\n\n/**\n * Processes HowTo supply into HowToSupply schema type\n * @param supply - String or HowToSupply object with or without @type\n * @returns HowToSupply with @type\n */\nexport function processHowToSupply(supply: Supply): HowToSupply {\n  if (isString(supply)) {\n    return {\n      \"@type\": SCHEMA_TYPES.HOW_TO_SUPPLY,\n      name: supply,\n    };\n  }\n\n  const processed = processSchemaType<HowToSupply>(\n    supply,\n    SCHEMA_TYPES.HOW_TO_SUPPLY,\n  );\n\n  // Process nested image if present\n  if (supply.image && !isString(supply.image)) {\n    processed.image = processImage(supply.image);\n  }\n\n  // Process nested estimatedCost if present\n  if (supply.estimatedCost && !isString(supply.estimatedCost)) {\n    processed.estimatedCost = processSimpleMonetaryAmount(supply.estimatedCost);\n  }\n\n  // Process nested requiredQuantity if present\n  if (supply.requiredQuantity && typeof supply.requiredQuantity === \"object\") {\n    processed.requiredQuantity = processQuantitativeValue(\n      supply.requiredQuantity,\n    );\n  }\n\n  return processed;\n}\n\n/**\n * Processes HowTo tool into HowToTool schema type\n * @param tool - String or HowToTool object with or without @type\n * @returns HowToTool with @type\n */\nexport function processHowToTool(tool: Tool): HowToTool {\n  if (isString(tool)) {\n    return {\n      \"@type\": SCHEMA_TYPES.HOW_TO_TOOL,\n      name: tool,\n    };\n  }\n\n  const processed = processSchemaType<HowToTool>(\n    tool,\n    SCHEMA_TYPES.HOW_TO_TOOL,\n  );\n\n  // Process nested image if present\n  if (tool.image && !isString(tool.image)) {\n    processed.image = processImage(tool.image);\n  }\n\n  // Process nested requiredQuantity if present\n  if (tool.requiredQuantity && typeof tool.requiredQuantity === \"object\") {\n    processed.requiredQuantity = processQuantitativeValue(\n      tool.requiredQuantity,\n    );\n  }\n\n  return processed;\n}\n\n/**\n * Processes estimated cost into MonetaryAmount schema type\n * @param cost - String or MonetaryAmount object with or without @type\n * @returns String or MonetaryAmount with @type\n */\nexport function processEstimatedCost(\n  cost: EstimatedCost,\n): string | SimpleMonetaryAmount {\n  if (isString(cost)) {\n    return cost;\n  }\n\n  return processSimpleMonetaryAmount(cost) as SimpleMonetaryAmount;\n}\n\n/**\n * Processes HowTo yield into string or QuantitativeValue schema type\n * @param yieldValue - String or QuantitativeValue object with or without @type\n * @returns String or QuantitativeValue with @type\n */\nexport function processHowToYield(\n  yieldValue: HowToYield,\n): string | QuantitativeValue {\n  if (isString(yieldValue)) {\n    return yieldValue;\n  }\n\n  return processQuantitativeValue(yieldValue);\n}\n"
  },
  {
    "path": "src/utils/stringify.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { stringify } from \"./stringify\";\n\ndescribe(\"stringify\", () => {\n  describe(\"URL handling\", () => {\n    it(\"preserves URLs with query parameters\", () => {\n      const data = {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Article\",\n        url: \"https://example.com/article?param1=value&param2=another\",\n        image:\n          \"https://example.com/image.jpg?width=1200&height=630&format=webp\",\n      };\n\n      const result = stringify(data);\n      const parsed = JSON.parse(result);\n\n      // URLs should not have & escaped to &amp;\n      expect(result).toContain(\"param1=value&param2=another\");\n      expect(result).toContain(\"width=1200&height=630&format=webp\");\n      expect(result).not.toContain(\"&amp;\");\n\n      // Verify data integrity\n      expect(parsed.url).toBe(\n        \"https://example.com/article?param1=value&param2=another\",\n      );\n      expect(parsed.image).toBe(\n        \"https://example.com/image.jpg?width=1200&height=630&format=webp\",\n      );\n    });\n\n    it(\"preserves Vercel og-image URLs\", () => {\n      const data = {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Article\",\n        image:\n          \"https://og-image.vercel.app/**Hello**%20World.png?theme=light&md=1&fontSize=100px&images=https://example.com/logo.png\",\n      };\n\n      const result = stringify(data);\n      const parsed = JSON.parse(result);\n\n      // Vercel og-image URL should remain intact\n      expect(result).toContain(\"theme=light&md=1&fontSize=100px\");\n      expect(result).not.toContain(\"&amp;\");\n      expect(parsed.image).toBe(data.image);\n    });\n\n    it(\"preserves complex URLs with multiple parameters\", () => {\n      const data = {\n        video: {\n          \"@type\": \"VideoObject\",\n          contentUrl:\n            \"https://example.com/video.mp4?quality=hd&autoplay=false&loop=true&muted=1\",\n          embedUrl:\n            \"https://youtube.com/embed/abc123?rel=0&showinfo=0&modestbranding=1\",\n        },\n      };\n\n      const result = stringify(data);\n      const parsed = JSON.parse(result);\n\n      expect(result).not.toContain(\"&amp;\");\n      expect(parsed.video.contentUrl).toBe(data.video.contentUrl);\n      expect(parsed.video.embedUrl).toBe(data.video.embedUrl);\n    });\n  });\n\n  describe(\"script tag escaping\", () => {\n    it(\"escapes </script> tags in strings\", () => {\n      const data = {\n        description: \"This contains a </script> tag\",\n      };\n\n      const result = stringify(data);\n\n      // Should escape the closing script tag using Unicode\n      expect(result).toContain(\"\\\\u003C/script>\");\n      expect(result).not.toContain(\"</script>\");\n\n      // Should still parse correctly\n      const parsed = JSON.parse(result);\n      // When parsed, the escaped sequence should be interpreted correctly\n      expect(parsed.description).toBe(\"This contains a </script> tag\");\n    });\n\n    it(\"escapes </SCRIPT> tags case-insensitively\", () => {\n      const data = {\n        description: \"This contains a </SCRIPT> tag in uppercase\",\n      };\n\n      const result = stringify(data);\n\n      // Case-insensitive regex converts all to lowercase\n      expect(result).toContain(\"\\\\u003C/script>\");\n      expect(result).not.toContain(\"</SCRIPT>\");\n      expect(result).not.toContain(\"</script>\");\n    });\n\n    it(\"escapes HTML comments\", () => {\n      const data = {\n        description: \"This has <!-- a comment --> inside\",\n      };\n\n      const result = stringify(data);\n\n      expect(result).toContain(\"\\\\u003C!--\");\n      expect(result).toContain(\"--\\\\u003E\");\n      expect(result).not.toContain(\"<!--\");\n      expect(result).not.toContain(\"-->\");\n\n      const parsed = JSON.parse(result);\n      expect(parsed.description).toBe(\"This has <!-- a comment --> inside\");\n    });\n\n    it(\"escapes multiple dangerous sequences\", () => {\n      const data = {\n        content:\n          \"<!-- Start --> Content with </script> and <!-- another comment --> and </SCRIPT>\",\n      };\n\n      const result = stringify(data);\n\n      expect(result).toContain(\"\\\\u003C!--\");\n      expect(result).toContain(\"--\\\\u003E\");\n      expect(result).toContain(\"\\\\u003C/script>\");\n      // All </script> tags are escaped the same way (case-insensitive)\n      // Both </script> and </SCRIPT> should be escaped to <\\\\/script\n      const scriptCount = (result.match(/\\\\u003C\\/script>/gi) || []).length;\n      expect(scriptCount).toBe(2);\n    });\n\n    it(\"escapes dangerous sequences in URLs\", () => {\n      const data = {\n        // This is an edge case - URLs shouldn't normally contain these sequences\n        url: \"https://example.com/page?content=</script>&comment=<!--test-->\",\n      };\n\n      const result = stringify(data);\n\n      // Even in URLs, dangerous sequences should be escaped\n      expect(result).toContain(\"\\\\u003C/script>\");\n      expect(result).toContain(\"\\\\u003C!--\");\n      expect(result).toContain(\"--\\\\u003E\");\n\n      // But regular & should not be escaped\n      expect(result).toContain(\"content=\\\\u003C/script>&comment=\");\n      expect(result).not.toContain(\"&amp;\");\n    });\n  });\n\n  describe(\"general behavior\", () => {\n    it(\"preserves special characters that are not dangerous\", () => {\n      const data = {\n        title: \"Article with <em>emphasis</em> & \\\"quotes\\\" and 'apostrophes'\",\n        price: \"$99.99 < $100\",\n      };\n\n      const result = stringify(data);\n      const parsed = JSON.parse(result);\n\n      // These characters should NOT be escaped\n      expect(result).toContain(\"<em>\");\n      expect(result).toContain(\"</em>\");\n      expect(result).toContain(\" & \");\n      expect(result).not.toContain(\"&amp;\");\n      expect(result).not.toContain(\"&lt;\");\n      expect(result).not.toContain(\"&gt;\");\n      expect(result).not.toContain(\"&quot;\");\n      expect(result).not.toContain(\"&apos;\");\n\n      expect(parsed.title).toBe(data.title);\n      expect(parsed.price).toBe(data.price);\n    });\n\n    it(\"handles nested objects and arrays\", () => {\n      const data = {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Recipe\",\n        name: \"Test Recipe\",\n        image: [\n          \"https://example.com/image1.jpg?size=small&format=jpg\",\n          \"https://example.com/image2.jpg?size=large&format=webp\",\n        ],\n        author: {\n          \"@type\": \"Person\",\n          name: \"Chef with </script> in name\",\n          url: \"https://example.com/chef?id=123&role=author\",\n        },\n      };\n\n      const result = stringify(data);\n      const parsed = JSON.parse(result);\n\n      // URLs in arrays should not be escaped\n      expect(result).not.toContain(\"&amp;\");\n\n      // But dangerous sequences should be\n      expect(result).toContain(\"\\\\u003C/script>\");\n\n      expect(parsed.image[0]).toBe(data.image[0]);\n      expect(parsed.image[1]).toBe(data.image[1]);\n      expect(parsed.author.url).toBe(data.author.url);\n      expect(parsed.author.name).toBe(\"Chef with </script> in name\");\n    });\n\n    it(\"omits null values\", () => {\n      const data = {\n        name: \"Test\",\n        value: null,\n        nested: {\n          prop: null,\n          other: \"value\",\n        },\n      };\n\n      const result = stringify(data);\n      const parsed = JSON.parse(result);\n\n      expect(parsed.value).toBeUndefined();\n      expect(parsed.nested.prop).toBeUndefined();\n      expect(parsed.nested.other).toBe(\"value\");\n    });\n\n    it(\"handles boolean and number values\", () => {\n      const data = {\n        isActive: true,\n        count: 42,\n        rating: 4.5,\n        isFree: false,\n      };\n\n      const result = stringify(data);\n      const parsed = JSON.parse(result);\n\n      expect(parsed.isActive).toBe(true);\n      expect(parsed.count).toBe(42);\n      expect(parsed.rating).toBe(4.5);\n      expect(parsed.isFree).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "src/utils/stringify.ts",
    "content": "/* eslint-disable */\n// Some of the code below is borrowed from react-schemaorg after the author of the package\n// kindly reached out to let me know this was a better way of doing things. ❤️\n// https://github.com/google/react-schemaorg/blob/main/src/json-ld.tsx#L173\n\ntype JsonValueScalar = string | boolean | number;\ntype JsonValue =\n  | JsonValueScalar\n  | Array<JsonValue>\n  | { [key: string]: JsonValue };\ntype JsonReplacer = (_: string, value: JsonValue) => JsonValue | undefined;\n\n/**\n * A replacer for JSON.stringify to omit null values from JSON-LD.\n * The actual script tag safety escaping is done in post-processing.\n */\nconst safeJsonLdReplacer: JsonReplacer = (() => {\n  return (_: string, value: JsonValue): JsonValue | undefined => {\n    switch (typeof value) {\n      case \"object\":\n        // Omit null values.\n        if (value === null) {\n          return undefined;\n        }\n        return value; // JSON.stringify will recursively call replacer.\n      case \"number\":\n      case \"boolean\":\n      case \"bigint\":\n      case \"string\":\n        return value; // Return all primitive values as-is\n      default: {\n        // We shouldn't expect other types.\n        isNever(value);\n        // JSON.stringify will remove this element.\n        return undefined;\n      }\n    }\n  };\n})();\n\n/**\n * Type guard to ensure exhaustive type checking.\n * @internal\n */\nfunction isNever(_: never): void {}\n\n/**\n * Stringify data for safe embedding in HTML script elements.\n *\n * Per W3C specifications and security best practices, we need to escape sequences\n * that could break out of the script tag:\n * - </script> sequences (case-insensitive)\n * - <!-- and --> sequences (HTML comments)\n *\n * We do NOT escape standard HTML entities like &, <, >, \", ' as they are valid\n * within script tag content and escaping them breaks URLs with query parameters.\n *\n * The escaping is done on the final JSON string to ensure the JSON remains valid\n * and parseable while being safe for HTML embedding.\n *\n * References:\n * - https://www.w3.org/TR/json-ld11/#restrictions-for-contents-of-json-ld-script-elements\n * - https://github.com/w3c/json-ld-syntax/issues/100\n */\nexport const stringify = (data: unknown) => {\n  const jsonString = JSON.stringify(data, safeJsonLdReplacer);\n\n  // Post-process the JSON string to escape dangerous sequences\n  // This ensures the JSON remains valid while being safe for script tags\n  // Use Unicode escape sequences to break up dangerous patterns\n  // This prevents the HTML parser from recognizing them while keeping valid JSON\n  return jsonString\n    .replace(/<\\/script>/gi, \"\\\\u003C/script>\") // Unicode escape for <\n    .replace(/<!--/g, \"\\\\u003C!--\") // Unicode escape for <\n    .replace(/-->/g, \"--\\\\u003E\"); // Unicode escape for >\n};\n"
  },
  {
    "path": "tests/e2e/.gitkeep",
    "content": " "
  },
  {
    "path": "tests/e2e/CLAUDE.md",
    "content": "# E2E Testing Guidelines\n\n## Critical Rules\n\n**ALL E2E tests must use real example pages!** Never mock or inject content in E2E tests.\n\n❌ **NEVER** use `page.route()` to inject mock HTML\n✅ **ALWAYS** test real pages from `examples/app-router-showcase/app/`\n\n## Commands\n\n```bash\npnpm build          # This must be run first to compile the library\npnpm test:e2e       # Run all E2E tests\npnpm test:e2e:ui    # Run with UI mode\npnpm example:dev    # Start example app on localhost:3001\n```\n\n## Test Structure\n\n1. **Navigate to real page**: `await page.goto(\"/article\")`\n2. **Extract JSON-LD**: Use `page.locator('script[type=\"application/ld+json\"]')`\n3. **Verify properties**: Test all expected Schema.org properties\n\n## Important Patterns\n\n### Security Testing\n\n- DO NOT add escape/security tests to individual component tests\n- Security testing is handled centrally in `security.e2e.spec.ts`\n- Focus on component-specific functionality only\n\n### Creating New Tests\n\nFor each new component E2E test:\n\n1. Create example pages in `examples/app-router-showcase/app/`\n2. Create test file: `[component]JsonLd.e2e.spec.ts`\n3. Add validation test in `jsonValidation.e2e.spec.ts`\n\n### Example Pages Required\n\n- Basic usage (minimal props)\n- Advanced usage (all features)\n- Each schema type variation\n- Special characters test (if applicable)\n\n## Test Pattern\n\n```typescript\ntest(\"renders basic [Component] structured data\", async ({ page }) => {\n  // Navigate to real example page\n  await page.goto(\"/[component]\");\n\n  const jsonLdScript = await page\n    .locator('script[type=\"application/ld+json\"]')\n    .textContent();\n\n  const jsonData = JSON.parse(jsonLdScript!);\n\n  // Verify properties\n  expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n  expect(jsonData[\"@type\"]).toBe(\"[Type]\");\n  // ... test all properties\n});\n```\n\n## Notes\n\n- Tests run against example app on port 3001\n- Example app auto-starts for E2E tests\n- Focus on testing actual rendered output\n- Verify JSON validity and structure\n"
  },
  {
    "path": "tests/e2e/aggregateRatingJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"AggregateRatingJsonLd\", () => {\n  test(\"renders basic aggregate rating for product\", async ({ page }) => {\n    await page.goto(\"/aggregate-rating\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"AggregateRating\");\n    expect(jsonData.itemReviewed).toEqual({\n      \"@type\": \"Product\",\n      name: \"Executive Anvil\",\n    });\n    expect(jsonData.ratingValue).toBe(4.4);\n    expect(jsonData.ratingCount).toBe(89);\n  });\n\n  test(\"renders restaurant aggregate rating with percentage scale\", async ({\n    page,\n  }) => {\n    await page.goto(\"/aggregate-rating-restaurant\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"AggregateRating\");\n    expect(jsonData.itemReviewed).toMatchObject({\n      \"@type\": \"LocalBusiness\",\n      name: \"Legal Seafood\",\n      servesCuisine: \"Seafood\",\n      priceRange: \"$$$\",\n      telephone: \"1234567\",\n    });\n\n    // Check address is properly formatted\n    expect(jsonData.itemReviewed.address).toEqual({\n      \"@type\": \"PostalAddress\",\n      streetAddress: \"123 William St\",\n      addressLocality: \"New York\",\n      addressRegion: \"NY\",\n      postalCode: \"10038\",\n      addressCountry: \"US\",\n    });\n\n    // Check percentage-based rating\n    expect(jsonData.ratingValue).toBe(88);\n    expect(jsonData.bestRating).toBe(100);\n    expect(jsonData.ratingCount).toBe(350);\n  });\n\n  test(\"renders aggregate rating nested in product\", async ({ page }) => {\n    await page.goto(\"/review-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Product should have nested aggregateRating\n    expect(jsonData[\"@type\"]).toBe(\"Product\");\n    expect(jsonData.aggregateRating).toBeDefined();\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.2,\n      bestRating: 5,\n      ratingCount: 150,\n      reviewCount: 120,\n    });\n  });\n\n  test(\"supports both ratingCount and reviewCount\", async ({ page }) => {\n    await page.goto(\"/review-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    const aggregateRating = jsonData.aggregateRating;\n\n    // Should have both ratingCount and reviewCount\n    expect(aggregateRating.ratingCount).toBe(150);\n    expect(aggregateRating.reviewCount).toBe(120);\n  });\n\n  test(\"properly handles various rating scales\", async ({ page }) => {\n    // Test default 1-5 scale\n    await page.goto(\"/aggregate-rating\");\n    let jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    let jsonData = JSON.parse(jsonLdScript!);\n\n    // Default scale (no bestRating/worstRating specified means 1-5)\n    expect(jsonData.ratingValue).toBe(4.4);\n    expect(jsonData.bestRating).toBeUndefined(); // Not specified, defaults to 5\n    expect(jsonData.worstRating).toBeUndefined(); // Not specified, defaults to 1\n\n    // Test percentage scale (0-100)\n    await page.goto(\"/aggregate-rating-restaurant\");\n    jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData.ratingValue).toBe(88);\n    expect(jsonData.bestRating).toBe(100);\n    expect(jsonData.worstRating).toBeUndefined(); // Can be undefined for 0-100 scale\n  });\n});\n"
  },
  {
    "path": "tests/e2e/articleJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"ArticleJsonLd\", () => {\n  test(\"renders basic Article structured data\", async ({ page }) => {\n    await page.goto(\"/article\");\n\n    // Find the JSON-LD script tag\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic Article properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Article\");\n    expect(jsonData.headline).toBe(\"Understanding Next.js App Router\");\n    expect(jsonData.url).toBe(\"https://example.com/articles/nextjs-app-router\");\n    expect(jsonData.datePublished).toBe(\"2024-01-01T08:00:00+00:00\");\n    expect(jsonData.dateModified).toBe(\"2024-01-01T08:00:00+00:00\"); // Should default to datePublished\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Sarah Johnson\",\n    });\n    expect(jsonData.image).toBe(\n      \"https://example.com/images/nextjs-article.jpg\",\n    );\n    expect(jsonData.description).toBe(\n      \"A comprehensive guide to Next.js App Router and its features\",\n    );\n  });\n\n  test(\"renders NewsArticle with multiple authors and images\", async ({\n    page,\n  }) => {\n    await page.goto(\"/news-article\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify NewsArticle type\n    expect(jsonData[\"@type\"]).toBe(\"NewsArticle\");\n    expect(jsonData.headline).toBe(\n      \"Breaking: Next.js 14 Released with Major Performance Improvements\",\n    );\n\n    // Verify multiple authors\n    expect(jsonData.author).toHaveLength(2);\n    expect(jsonData.author[0]).toEqual({\n      \"@type\": \"Person\",\n      name: \"Alex Chen\",\n      url: \"https://example.com/authors/alex-chen\",\n    });\n    expect(jsonData.author[1]).toEqual({\n      \"@type\": \"Person\",\n      name: \"Maria Garcia\",\n      url: \"https://example.com/authors/maria-garcia\",\n    });\n\n    // Verify multiple images\n    expect(jsonData.image).toHaveLength(3);\n    expect(jsonData.image).toContain(\n      \"https://example.com/images/nextjs-14-16x9.jpg\",\n    );\n    expect(jsonData.image).toContain(\n      \"https://example.com/images/nextjs-14-4x3.jpg\",\n    );\n    expect(jsonData.image).toContain(\n      \"https://example.com/images/nextjs-14-1x1.jpg\",\n    );\n\n    // Verify publisher with logo\n    expect(jsonData.publisher).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Tech News Daily\",\n      logo: {\n        \"@type\": \"ImageObject\",\n        url: \"https://example.com/logo.png\",\n        width: 600,\n        height: 60,\n      },\n    });\n\n    // Verify dates\n    expect(jsonData.datePublished).toBe(\"2024-01-15T10:00:00+00:00\");\n    expect(jsonData.dateModified).toBe(\"2024-01-15T14:30:00+00:00\");\n\n    // Verify accessibility\n    expect(jsonData.isAccessibleForFree).toBe(true);\n  });\n\n  test(\"renders BlogPosting with all properties\", async ({ page }) => {\n    await page.goto(\"/blog-posting\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify BlogPosting type\n    expect(jsonData[\"@type\"]).toBe(\"BlogPosting\");\n\n    // Verify Organization author\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Organization\",\n      name: \"WebDev Solutions\",\n      url: \"https://example.com\",\n      logo: \"https://example.com/webdev-logo.png\",\n    });\n\n    // Verify ImageObject\n    expect(jsonData.image).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/images/seo-tips-hero.jpg\",\n      width: 1920,\n      height: 1080,\n      caption: \"SEO Tips for Web Developers\",\n    });\n\n    // Verify mainEntityOfPage\n    expect(jsonData.mainEntityOfPage).toEqual({\n      \"@type\": \"WebPage\",\n      \"@id\": \"https://example.com/blog/seo-tips-web-development\",\n    });\n\n    // Verify premium content flag\n    expect(jsonData.isAccessibleForFree).toBe(false);\n  });\n\n  test(\"renders multiple JSON-LD scripts on the same page\", async ({\n    page,\n  }) => {\n    // Navigate to a page and inject multiple ArticleJsonLd components\n    await page.goto(\"/article\");\n\n    // Count JSON-LD scripts\n    const scriptsCount = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .count();\n    expect(scriptsCount).toBeGreaterThanOrEqual(1);\n\n    // Verify each script contains valid JSON\n    const scripts = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .all();\n    for (const script of scripts) {\n      const content = await script.textContent();\n      expect(() => JSON.parse(content!)).not.toThrow();\n    }\n  });\n});\n"
  },
  {
    "path": "tests/e2e/breadcrumbJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"BreadcrumbJsonLd\", () => {\n  test(\"renders basic breadcrumb structured data\", async ({ page }) => {\n    await page.goto(\"/breadcrumb\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"BreadcrumbList\");\n    expect(jsonData.itemListElement).toHaveLength(5);\n\n    // Verify first item\n    expect(jsonData.itemListElement[0]).toEqual({\n      \"@type\": \"ListItem\",\n      position: 1,\n      name: \"Home\",\n      item: \"https://example.com\",\n    });\n\n    // Verify middle item\n    expect(jsonData.itemListElement[2]).toEqual({\n      \"@type\": \"ListItem\",\n      position: 3,\n      name: \"Electronics\",\n      item: \"https://example.com/products/electronics\",\n    });\n\n    // Verify last item (no URL)\n    expect(jsonData.itemListElement[4]).toEqual({\n      \"@type\": \"ListItem\",\n      position: 5,\n      name: \"iPhone 15 Pro\",\n    });\n  });\n\n  test(\"renders multiple breadcrumb trails\", async ({ page }) => {\n    await page.goto(\"/breadcrumb/multiple\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Should be an array of two BreadcrumbList objects\n    expect(Array.isArray(jsonData)).toBe(true);\n    expect(jsonData).toHaveLength(2);\n\n    // Verify first trail\n    expect(jsonData[0][\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[0][\"@type\"]).toBe(\"BreadcrumbList\");\n    expect(jsonData[0].itemListElement).toHaveLength(3);\n    expect(jsonData[0].itemListElement[0].name).toBe(\"Books\");\n    expect(jsonData[0].itemListElement[1].name).toBe(\"Science Fiction\");\n    expect(jsonData[0].itemListElement[2].name).toBe(\"Award Winners\");\n\n    // Verify second trail\n    expect(jsonData[1][\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[1][\"@type\"]).toBe(\"BreadcrumbList\");\n    expect(jsonData[1].itemListElement).toHaveLength(2);\n    expect(jsonData[1].itemListElement[0].name).toBe(\"Literature\");\n    expect(jsonData[1].itemListElement[1].name).toBe(\"Award Winners\");\n  });\n\n  test(\"renders with Thing objects and custom attributes\", async ({ page }) => {\n    await page.goto(\"/breadcrumb/advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"][id=\"blog-breadcrumb\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify Thing objects with @id\n    expect(jsonData.itemListElement[1].item).toEqual({\n      \"@id\": \"https://example.com/blog\",\n    });\n    expect(jsonData.itemListElement[2].item).toEqual({\n      \"@id\": \"https://example.com/blog/technology\",\n    });\n\n    // Verify mixed usage (URL string for first item)\n    expect(jsonData.itemListElement[0].item).toBe(\"https://example.com\");\n\n    // Verify last item has no URL\n    expect(jsonData.itemListElement[3].item).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "tests/e2e/carouselJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"CarouselJsonLd\", () => {\n  test(\"renders summary page carousel structured data\", async ({ page }) => {\n    await page.goto(\"/carousel-summary\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify ItemList structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"ItemList\");\n    expect(jsonData.itemListElement).toHaveLength(5);\n\n    // Verify first item (simple URL string)\n    expect(jsonData.itemListElement[0]).toEqual({\n      \"@type\": \"ListItem\",\n      position: 1,\n      url: \"https://example.com/recipe/chocolate-cookies\",\n    });\n\n    // Verify second item (auto-positioned)\n    expect(jsonData.itemListElement[1]).toEqual({\n      \"@type\": \"ListItem\",\n      position: 2,\n      url: \"https://example.com/recipe/banana-bread\",\n    });\n\n    // Verify third item (custom position)\n    expect(jsonData.itemListElement[2]).toEqual({\n      \"@type\": \"ListItem\",\n      position: 3,\n      url: \"https://example.com/recipe/apple-pie\",\n    });\n\n    // Verify positions are correct\n    expect(jsonData.itemListElement[3].position).toBe(4);\n    expect(jsonData.itemListElement[4].position).toBe(5);\n  });\n\n  test(\"renders Course carousel with full data\", async ({ page }) => {\n    await page.goto(\"/carousel-course\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"ItemList\");\n    expect(jsonData.itemListElement).toHaveLength(4);\n\n    // Verify first course with string provider\n    const firstCourse = jsonData.itemListElement[0];\n    expect(firstCourse[\"@type\"]).toBe(\"ListItem\");\n    expect(firstCourse.position).toBe(1);\n    expect(firstCourse.item[\"@type\"]).toBe(\"Course\");\n    expect(firstCourse.item.name).toBe(\"Introduction to Web Development\");\n    expect(firstCourse.item.description).toContain(\"Learn the fundamentals\");\n    expect(firstCourse.item.provider).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Tech Academy Online\",\n    });\n\n    // Verify second course with object provider\n    const secondCourse = jsonData.itemListElement[1];\n    expect(secondCourse.item.provider[\"@type\"]).toBe(\"Organization\");\n    expect(secondCourse.item.provider.name).toBe(\"Code School Pro\");\n    expect(secondCourse.item.provider.url).toBe(\"https://example.com/school\");\n\n    // Verify third course with provider having sameAs\n    const thirdCourse = jsonData.itemListElement[2];\n    expect(thirdCourse.item.provider.sameAs).toEqual([\n      \"https://twitter.com/devinstitute\",\n    ]);\n  });\n\n  test(\"renders Movie carousel with directors and reviews\", async ({\n    page,\n  }) => {\n    await page.goto(\"/carousel-movie\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"ItemList\");\n    expect(jsonData.itemListElement).toHaveLength(4);\n\n    // Verify The Matrix movie\n    const matrix = jsonData.itemListElement[0].item;\n    expect(matrix[\"@type\"]).toBe(\"Movie\");\n    expect(matrix.name).toBe(\"The Matrix\");\n    expect(Array.isArray(matrix.image)).toBe(true);\n    expect(matrix.image).toHaveLength(2);\n    expect(matrix.director).toEqual({\n      \"@type\": \"Person\",\n      name: \"The Wachowskis\",\n    });\n    expect(matrix.aggregateRating[\"@type\"]).toBe(\"AggregateRating\");\n    expect(matrix.aggregateRating.ratingValue).toBe(8.7);\n    expect(matrix.review[\"@type\"]).toBe(\"Review\");\n    expect(matrix.review.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Film Critic Daily\",\n    });\n\n    // Verify Inception with object director\n    const inception = jsonData.itemListElement[1].item;\n    expect(inception.director[\"@type\"]).toBe(\"Person\");\n    expect(inception.director.name).toBe(\"Christopher Nolan\");\n    expect(inception.director.url).toBe(\"https://example.com/directors/nolan\");\n\n    // Verify Interstellar with multiple images\n    const interstellar = jsonData.itemListElement[2].item;\n    expect(interstellar.image).toHaveLength(3);\n    expect(interstellar.review.reviewRating[\"@type\"]).toBe(\"Rating\");\n    expect(interstellar.review.reviewRating.bestRating).toBe(5);\n  });\n\n  test(\"renders Recipe carousel with full recipe details\", async ({ page }) => {\n    await page.goto(\"/carousel-recipe\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"ItemList\");\n    expect(jsonData.itemListElement).toHaveLength(3);\n\n    // Verify chocolate chip cookies\n    const cookies = jsonData.itemListElement[0].item;\n    expect(cookies[\"@type\"]).toBe(\"Recipe\");\n    expect(cookies.name).toBe(\"Perfect Chocolate Chip Cookies\");\n    expect(Array.isArray(cookies.image)).toBe(true);\n    expect(cookies.image).toHaveLength(3);\n    expect(cookies.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Chef Sarah\",\n    });\n    expect(cookies.recipeYield).toBe(\"24 cookies\");\n    expect(cookies.recipeIngredient).toHaveLength(9);\n    expect(cookies.recipeInstructions).toHaveLength(8);\n    expect(cookies.nutrition[\"@type\"]).toBe(\"NutritionInformation\");\n    expect(cookies.nutrition.calories).toBe(\"210 calories\");\n    expect(cookies.aggregateRating[\"@type\"]).toBe(\"AggregateRating\");\n\n    // Verify banana bread with author and video\n    const bananaBread = jsonData.itemListElement[1].item;\n    expect(bananaBread.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Grandma Rose\",\n    });\n    expect(bananaBread.recipeInstructions[\"@type\"]).toBe(\"HowToSection\");\n    expect(bananaBread.recipeInstructions.itemListElement).toHaveLength(6);\n    expect(bananaBread.video[\"@type\"]).toBe(\"VideoObject\");\n    expect(bananaBread.video.duration).toBe(\"PT8M\");\n\n    // Verify pizza with HowToStep instructions\n    const pizza = jsonData.itemListElement[2].item;\n    expect(pizza.image[\"@type\"]).toBe(\"ImageObject\");\n    expect(pizza.image.width).toBe(1200);\n    expect(pizza.recipeInstructions[0][\"@type\"]).toBe(\"HowToStep\");\n    expect(pizza.recipeInstructions[0].name).toBe(\"Prepare dough\");\n  });\n\n  test(\"renders Restaurant carousel with address and opening hours\", async ({\n    page,\n  }) => {\n    await page.goto(\"/carousel-restaurant\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"ItemList\");\n    expect(jsonData.itemListElement).toHaveLength(4);\n\n    // Verify Luigi's with string address\n    const luigis = jsonData.itemListElement[0].item;\n    expect(luigis[\"@type\"]).toBe(\"Restaurant\");\n    expect(luigis.name).toBe(\"Luigi's Italian Bistro\");\n    expect(luigis.address).toBe(\"123 Main Street, New York, NY 10001\");\n    expect(luigis.priceRange).toBe(\"$$$\");\n    expect(Array.isArray(luigis.servesCuisine)).toBe(true);\n    expect(luigis.servesCuisine).toContain(\"Italian\");\n    expect(luigis.geo[\"@type\"]).toBe(\"GeoCoordinates\");\n    expect(luigis.geo.latitude).toBe(40.7489);\n    expect(luigis.openingHoursSpecification).toHaveLength(3);\n    expect(luigis.openingHoursSpecification[0][\"@type\"]).toBe(\n      \"OpeningHoursSpecification\",\n    );\n    expect(luigis.review[\"@type\"]).toBe(\"Review\");\n    expect(luigis.sameAs).toHaveLength(2);\n\n    // Verify Sakura with PostalAddress object\n    const sakura = jsonData.itemListElement[1].item;\n    expect(sakura.address[\"@type\"]).toBe(\"PostalAddress\");\n    expect(sakura.address.streetAddress).toBe(\"456 Oak Avenue\");\n    expect(sakura.address.addressLocality).toBe(\"San Francisco\");\n    expect(sakura.address.postalCode).toBe(\"94102\");\n    expect(sakura.openingHoursSpecification[\"@type\"]).toBe(\n      \"OpeningHoursSpecification\",\n    );\n\n    // Verify Garden Terrace with multiple reviews\n    const garden = jsonData.itemListElement[2].item;\n    expect(garden.image[\"@type\"]).toBe(\"ImageObject\");\n    expect(Array.isArray(garden.review)).toBe(true);\n    expect(garden.review).toHaveLength(2);\n    expect(garden.review[0].author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Michelin Guide\",\n    });\n    expect(garden.review[1].author[\"@type\"]).toBe(\"Person\");\n\n    // Verify Taco Paradise\n    const taco = jsonData.itemListElement[3].item;\n    expect(Array.isArray(taco.image)).toBe(true);\n    expect(taco.image).toHaveLength(3);\n    expect(taco.priceRange).toBe(\"$\");\n  });\n\n  test(\"properly escapes special characters in carousel content\", async ({\n    page,\n  }) => {\n    // Create a test page with special characters\n    await page.goto(\"/carousel-recipe\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n\n    // Should be valid JSON even with special characters in content\n    expect(() => JSON.parse(jsonLdScript!)).not.toThrow();\n\n    // Check that content with quotes and special chars is properly handled\n    const jsonData = JSON.parse(jsonLdScript!);\n    expect(jsonData.itemListElement[0].item.recipeIngredient).toContain(\n      \"2 1/4 cups all-purpose flour\",\n    );\n  });\n});\n"
  },
  {
    "path": "tests/e2e/claimReviewJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"ClaimReviewJsonLd\", () => {\n  test(\"renders basic ClaimReview structured data\", async ({ page }) => {\n    await page.goto(\"/claim-review\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify all properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"ClaimReview\");\n    expect(jsonData.claimReviewed).toBe(\"The world is flat\");\n    expect(jsonData.url).toBe(\n      \"https://example.com/news/science/worldisflat.html\",\n    );\n\n    // Verify rating\n    expect(jsonData.reviewRating).toEqual({\n      \"@type\": \"Rating\",\n      ratingValue: 1,\n      bestRating: 5,\n      worstRating: 1,\n      alternateName: \"False\",\n    });\n\n    // Verify author\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Example.com science watch\",\n    });\n  });\n\n  test(\"renders advanced ClaimReview with full claim details\", async ({\n    page,\n  }) => {\n    await page.goto(\"/claim-review-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify organization author\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Example.com Science Watch\",\n      url: \"https://example.com/science\",\n      logo: \"https://example.com/logo.jpg\",\n    });\n\n    // Verify rating with name\n    expect(jsonData.reviewRating.name).toBe(\"False\");\n\n    // Verify itemReviewed\n    expect(jsonData.itemReviewed).toBeTruthy();\n    expect(jsonData.itemReviewed[\"@type\"]).toBe(\"Claim\");\n    expect(jsonData.itemReviewed.author).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Square World Society\",\n      sameAs: \"https://example.flatworlders.com/we-know-that-the-world-is-flat\",\n    });\n    expect(jsonData.itemReviewed.datePublished).toBe(\"2024-06-20\");\n\n    // Verify appearance\n    expect(jsonData.itemReviewed.appearance).toEqual({\n      \"@type\": \"OpinionNewsArticle\",\n      url: \"https://example.com/news/a122121\",\n      headline: \"Square Earth - Flat earthers for the Internet age\",\n      datePublished: \"2024-06-22\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"T. Tellar\",\n      },\n      image: \"https://example.com/photos/1x1/photo.jpg\",\n      publisher: {\n        \"@type\": \"Organization\",\n        name: \"Skeptical News\",\n        logo: {\n          \"@type\": \"ImageObject\",\n          url: \"https://example.com/logo.jpg\",\n        },\n      },\n    });\n  });\n\n  test(\"renders ClaimReview with organization author and firstAppearance\", async ({\n    page,\n  }) => {\n    await page.goto(\"/claim-review-organization\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify claim\n    expect(jsonData.claimReviewed).toBe(\"Climate change is not real\");\n\n    // Verify rating\n    expect(jsonData.reviewRating.alternateName).toBe(\"Pants on Fire\");\n\n    // Verify organization author with detailed properties\n    expect(jsonData.author[\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.author.name).toBe(\"Climate Facts Organization\");\n    expect(jsonData.author.logo).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/logo.png\",\n      width: 300,\n      height: 60,\n    });\n    expect(jsonData.author.sameAs).toEqual([\n      \"https://twitter.com/climatefacts\",\n      \"https://facebook.com/climatefacts\",\n    ]);\n\n    // Verify itemReviewed with organization claim author\n    expect(jsonData.itemReviewed.author).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Climate Denial Institute\",\n      url: \"https://example-denial.com\",\n    });\n\n    // Verify firstAppearance\n    expect(jsonData.itemReviewed.firstAppearance).toEqual({\n      \"@type\": \"CreativeWork\",\n      url: \"https://example-denial.com/climate-hoax\",\n      headline: \"The Great Climate Hoax Exposed\",\n      datePublished: \"2024-07-01\",\n      author: {\n        \"@type\": \"Organization\",\n        name: \"Climate Denial Institute\",\n        url: \"https://example-denial.com\",\n      },\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e/courseJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"CourseJsonLd\", () => {\n  test(\"renders single Course structured data\", async ({ page }) => {\n    await page.goto(\"/course\");\n\n    // Find the JSON-LD script tag\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify Course properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Course\");\n    expect(jsonData.name).toBe(\n      \"Introduction to Computer Science and Programming\",\n    );\n    expect(jsonData.description).toBe(\n      \"This is an introductory CS course laying out the basics.\",\n    );\n    expect(jsonData.url).toBe(\"https://example.com/courses/intro-cs\");\n    expect(jsonData.provider).toEqual({\n      \"@type\": \"Organization\",\n      name: \"University of Technology - Eureka\",\n      sameAs: \"https://www.example.com\",\n    });\n  });\n\n  test(\"renders Course list with full data (all-in-one pattern)\", async ({\n    page,\n  }) => {\n    await page.goto(\"/course-list\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify ItemList structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"ItemList\");\n    expect(jsonData.itemListElement).toHaveLength(3);\n\n    // Verify first course\n    const firstItem = jsonData.itemListElement[0];\n    expect(firstItem[\"@type\"]).toBe(\"ListItem\");\n    expect(firstItem.position).toBe(1);\n    expect(firstItem.item).toEqual({\n      \"@type\": \"Course\",\n      name: \"Introduction to Computer Science and Programming\",\n      description: \"This is an introductory CS course laying out the basics.\",\n      url: \"https://example.com/courses#intro-to-cs\",\n      provider: {\n        \"@type\": \"Organization\",\n        name: \"University of Technology - Example\",\n        sameAs: \"https://www.example.com\",\n      },\n    });\n\n    // Verify second course\n    const secondItem = jsonData.itemListElement[1];\n    expect(secondItem.position).toBe(2);\n    expect(secondItem.item.name).toBe(\n      \"Intermediate Computer Science and Programming\",\n    );\n    expect(secondItem.item.description).toBe(\n      \"This CS course builds on the basics from the intro course.\",\n    );\n\n    // Verify third course\n    const thirdItem = jsonData.itemListElement[2];\n    expect(thirdItem.position).toBe(3);\n    expect(thirdItem.item.name).toBe(\n      \"Advanced Computer Science and Programming\",\n    );\n    expect(thirdItem.item.provider.name).toBe(\n      \"University of Technology - Eureka\",\n    );\n  });\n\n  test(\"renders Course list with URLs only (summary page pattern)\", async ({\n    page,\n  }) => {\n    await page.goto(\"/course-list-summary\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify ItemList structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"ItemList\");\n    expect(jsonData.itemListElement).toHaveLength(5);\n\n    // Verify URLs are preserved correctly\n    const urls = jsonData.itemListElement.map(\n      (item: { url: string }) => item.url,\n    );\n    expect(urls).toEqual([\n      \"https://example.com/courses/intro-programming\",\n      \"https://example.com/courses/web-development\",\n      \"https://example.com/courses/data-science\",\n      \"https://example.com/courses/machine-learning\",\n      \"https://example.com/courses/mobile-development\",\n    ]);\n\n    // Verify each item has proper structure\n    jsonData.itemListElement.forEach(\n      (\n        item: {\n          \"@type\": string;\n          position: number;\n          url: string;\n          item?: unknown;\n        },\n        index: number,\n      ) => {\n        expect(item[\"@type\"]).toBe(\"ListItem\");\n        expect(item.position).toBe(index + 1);\n        expect(item.url).toBeTruthy();\n        expect(item.item).toBeUndefined(); // Should not have item property for summary pattern\n      },\n    );\n  });\n\n  test(\"preserves URL query parameters in course URLs\", async ({ page }) => {\n    // Create a test page with query parameters\n    await page.goto(\"/course\");\n\n    // Inject a script to modify the CourseJsonLd\n    await page.evaluate(() => {\n      const script = document.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      if (script) {\n        const data = JSON.parse(script.textContent || \"{}\");\n        data.url =\n          \"https://example.com/courses/test?utm_source=google&ref=home\";\n        script.textContent = JSON.stringify(data);\n      }\n    });\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData.url).toBe(\n      \"https://example.com/courses/test?utm_source=google&ref=home\",\n    );\n  });\n});\n"
  },
  {
    "path": "tests/e2e/creativeWorkJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"CreativeWorkJsonLd\", () => {\n  test(\"renders basic Article with paywalled content\", async ({ page }) => {\n    await page.goto(\"/creative-work\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Article\");\n    expect(jsonData.headline).toBe(\n      \"Premium Article: Understanding Paywalled Content\",\n    );\n    expect(jsonData.url).toBe(\"https://example.com/articles/premium-content\");\n    expect(jsonData.datePublished).toBe(\"2024-01-01T08:00:00+00:00\");\n    expect(jsonData.dateModified).toBe(\"2024-01-02T10:00:00+00:00\");\n\n    // Verify author processing\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Sarah Johnson\",\n    });\n\n    // Verify publisher\n    expect(jsonData.publisher).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Premium Publications\",\n      logo: \"https://example.com/logo.png\",\n    });\n\n    // Verify paywall marking\n    expect(jsonData.isAccessibleForFree).toBe(false);\n    expect(jsonData.hasPart).toEqual({\n      \"@type\": \"WebPageElement\",\n      isAccessibleForFree: false,\n      cssSelector: \".paywall\",\n    });\n\n    expect(jsonData.mainEntityOfPage).toBe(\"https://example.com/articles\");\n  });\n\n  test(\"renders Article with multiple paywalled sections\", async ({ page }) => {\n    await page.goto(\"/creative-work-multiple\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Article\");\n    expect(jsonData.headline).toBe(\n      \"In-Depth Analysis: Multiple Premium Sections\",\n    );\n\n    // Verify multiple authors\n    expect(jsonData.author).toHaveLength(2);\n    expect(jsonData.author[0]).toEqual({\n      \"@type\": \"Person\",\n      name: \"Dr. Emily Chen\",\n    });\n    expect(jsonData.author[1][\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.author[1].name).toBe(\"Research Institute\");\n\n    // Verify multiple images\n    expect(jsonData.image).toHaveLength(3);\n    expect(jsonData.image[0]).toBe(\n      \"https://example.com/images/analysis-16x9.jpg\",\n    );\n\n    // Verify multiple paywalled sections\n    expect(jsonData.hasPart).toHaveLength(2);\n    expect(jsonData.hasPart[0]).toEqual({\n      \"@type\": \"WebPageElement\",\n      isAccessibleForFree: false,\n      cssSelector: \".section1\",\n    });\n    expect(jsonData.hasPart[1]).toEqual({\n      \"@type\": \"WebPageElement\",\n      isAccessibleForFree: false,\n      cssSelector: \".section2\",\n    });\n  });\n\n  test(\"renders NewsArticle with premium content\", async ({ page }) => {\n    await page.goto(\"/creative-work-news\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"NewsArticle\");\n    expect(jsonData.headline).toBe(\n      \"Breaking: Major Scientific Discovery Behind Paywall\",\n    );\n\n    // Verify author with URL\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Jane Martinez\",\n      url: \"https://example.com/journalists/jane-martinez\",\n    });\n\n    // Verify ImageObject\n    expect(jsonData.image[\"@type\"]).toBe(\"ImageObject\");\n    expect(jsonData.image.url).toBe(\n      \"https://example.com/images/discovery-hero.jpg\",\n    );\n    expect(jsonData.image.width).toBe(1200);\n    expect(jsonData.image.height).toBe(630);\n    expect(jsonData.image.caption).toBe(\"Scientific breakthrough illustration\");\n\n    // Verify publisher with logo ImageObject\n    expect(jsonData.publisher.name).toBe(\"Global News Network\");\n    expect(jsonData.publisher.logo[\"@type\"]).toBe(\"ImageObject\");\n    expect(jsonData.publisher.logo.url).toBe(\n      \"https://example.com/gnn-logo.png\",\n    );\n\n    // Verify paywall section\n    expect(jsonData.isAccessibleForFree).toBe(false);\n    expect(jsonData.hasPart).toEqual({\n      \"@type\": \"WebPageElement\",\n      isAccessibleForFree: false,\n      cssSelector: \".premium-news\",\n    });\n\n    // Verify mainEntityOfPage as WebPage\n    expect(jsonData.mainEntityOfPage).toEqual({\n      \"@type\": \"WebPage\",\n      \"@id\": \"https://example.com/news/scientific-discovery\",\n    });\n  });\n\n  test(\"renders Blog with subscription content\", async ({ page }) => {\n    await page.goto(\"/creative-work-blog\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Blog\");\n    expect(jsonData.name).toBe(\"Premium Tech Insights Blog\");\n    expect(jsonData.url).toBe(\"https://example.com/blog\");\n    expect(jsonData.description).toContain(\"premium technology blog\");\n\n    // Verify it uses name instead of headline for Blog type\n    expect(jsonData.headline).toBeUndefined();\n\n    // Verify author and publisher\n    expect(jsonData.author[\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.author.name).toBe(\"Tech Insights Team\");\n    expect(jsonData.publisher.name).toBe(\"Tech Insights Publishing\");\n\n    // Verify subscription marking\n    expect(jsonData.isAccessibleForFree).toBe(false);\n    expect(jsonData.hasPart).toEqual({\n      \"@type\": \"WebPageElement\",\n      isAccessibleForFree: false,\n      cssSelector: \".members-only\",\n    });\n\n    // Verify Blog doesn't auto-add dateModified\n    expect(jsonData.datePublished).toBe(\"2024-01-01T00:00:00+00:00\");\n    expect(jsonData.dateModified).toBeUndefined();\n  });\n\n  test(\"properly escapes special characters in content\", async ({ page }) => {\n    // Navigate to the creative-work page which has content with special characters\n    await page.goto(\"/creative-work\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n\n    // Verify JSON is valid even with special characters\n    expect(() => JSON.parse(jsonLdScript!)).not.toThrow();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n    expect(jsonData.description).toContain(\"paywalled content\");\n  });\n\n  test(\"verifies all CreativeWork types are supported\", async ({ page }) => {\n    // Test the basic creative-work page\n    await page.goto(\"/creative-work\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify the component supports different types\n    const supportedTypes = [\n      \"CreativeWork\",\n      \"Article\",\n      \"NewsArticle\",\n      \"Blog\",\n      \"BlogPosting\",\n      \"Comment\",\n      \"Course\",\n      \"HowTo\",\n      \"Message\",\n      \"Review\",\n      \"WebPage\",\n    ];\n\n    // The current page uses \"Article\" type\n    expect(supportedTypes).toContain(jsonData[\"@type\"]);\n  });\n});\n"
  },
  {
    "path": "tests/e2e/customComponents.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"Custom JSON-LD Components\", () => {\n  test(\"renders PodcastSeries structured data correctly\", async ({ page }) => {\n    await page.goto(\"/custom-podcast\");\n\n    // Get the JSON-LD script content\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n\n    expect(jsonLdScript).toBeTruthy();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"PodcastSeries\");\n    expect(jsonData.name).toBe(\"Tech Talk Weekly\");\n    expect(jsonData.description).toBe(\n      \"Weekly discussions about technology trends and innovations\",\n    );\n    expect(jsonData.url).toBe(\"https://example.com/podcasts/tech-talk-weekly\");\n\n    // Verify processor worked - host string became Person with @type\n    expect(jsonData.host).toEqual({\n      \"@type\": \"Person\",\n      name: \"Sarah Johnson\",\n    });\n\n    // Verify image processor kept string as-is\n    expect(jsonData.image).toBe(\"https://example.com/podcast-cover.jpg\");\n\n    // Verify episodes array structure\n    expect(jsonData.episode).toHaveLength(3);\n    expect(jsonData.episode[0]).toEqual({\n      \"@type\": \"PodcastEpisode\",\n      name: \"Episode 1: AI Revolution\",\n      position: 1,\n      duration: \"PT30M\",\n      datePublished: \"2024-01-01\",\n      description:\n        \"Exploring the latest developments in artificial intelligence\",\n      url: \"https://example.com/episodes/ep1-ai-revolution\",\n    });\n\n    // Verify position is auto-generated\n    expect(jsonData.episode[1].position).toBe(2);\n    expect(jsonData.episode[2].position).toBe(3);\n  });\n\n  test(\"renders Service structured data correctly\", async ({ page }) => {\n    await page.goto(\"/custom-service\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n\n    expect(jsonLdScript).toBeTruthy();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Service\");\n    expect(jsonData.name).toBe(\"Web Development Services\");\n    expect(jsonData.serviceType).toBe(\"Professional Service\");\n    expect(jsonData.description).toContain(\"Full-stack web development\");\n    expect(jsonData.url).toBe(\"https://example.com/services/web-development\");\n\n    // Verify processor worked - complex provider object became Organization with @type\n    expect(jsonData.provider).toMatchObject({\n      \"@type\": \"Organization\",\n      name: \"Tech Solutions Inc\",\n      url: \"https://example.com\",\n      logo: expect.any(String),\n      address: expect.objectContaining({\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"123 Tech Street\",\n        addressLocality: \"San Francisco\",\n        addressRegion: \"CA\",\n        postalCode: \"94105\",\n        addressCountry: \"US\",\n      }),\n    });\n\n    // Verify areaServed is array\n    expect(jsonData.areaServed).toEqual([\"US\", \"CA\", \"UK\", \"AU\"]);\n\n    // Verify offers structure\n    expect(jsonData.offers).toEqual({\n      \"@type\": \"Offer\",\n      priceRange: \"$$$\",\n    });\n\n    // Verify aggregateRating processor worked\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.8,\n      reviewCount: 127,\n      bestRating: 5,\n      worstRating: 1,\n    });\n  });\n\n  test(\"processors handle flexible input types correctly\", async ({ page }) => {\n    // Test PodcastSeries with string inputs\n    await page.goto(\"/custom-podcast\");\n\n    let jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    let jsonData = JSON.parse(jsonLdScript!);\n\n    // String host became Person object with @type\n    expect(jsonData.host).toHaveProperty(\"@type\", \"Person\");\n    expect(jsonData.host).toHaveProperty(\"name\", \"Sarah Johnson\");\n\n    // String image URL stayed as string (processImage preserves strings)\n    expect(typeof jsonData.image).toBe(\"string\");\n\n    // Test Service with complex inputs\n    await page.goto(\"/custom-service\");\n\n    jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    jsonData = JSON.parse(jsonLdScript!);\n\n    // Complex provider object has @type added\n    expect(jsonData.provider).toHaveProperty(\"@type\", \"Organization\");\n\n    // Nested address also has @type added by processor\n    expect(jsonData.provider.address).toHaveProperty(\"@type\", \"PostalAddress\");\n\n    // Single value or array handling for areaServed\n    expect(Array.isArray(jsonData.areaServed)).toBe(true);\n  });\n\n  test(\"custom components follow @type optional pattern\", async ({ page }) => {\n    // Verify that developers don't need to specify @type manually\n    await page.goto(\"/custom-podcast\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // All nested objects should have @type added automatically\n    expect(jsonData[\"@type\"]).toBe(\"PodcastSeries\");\n    expect(jsonData.host[\"@type\"]).toBe(\"Person\");\n    jsonData.episode.forEach((ep: { \"@type\": string }) => {\n      expect(ep[\"@type\"]).toBe(\"PodcastEpisode\");\n    });\n\n    // Service page\n    await page.goto(\"/custom-service\");\n\n    const serviceScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const serviceData = JSON.parse(serviceScript!);\n\n    // Verify all types are properly set\n    expect(serviceData[\"@type\"]).toBe(\"Service\");\n    expect(serviceData.provider[\"@type\"]).toBe(\"Organization\");\n    expect(serviceData.provider.address[\"@type\"]).toBe(\"PostalAddress\");\n    expect(serviceData.offers[\"@type\"]).toBe(\"Offer\");\n    expect(serviceData.aggregateRating[\"@type\"]).toBe(\"AggregateRating\");\n  });\n\n  test(\"JSON-LD output is valid and parseable\", async ({ page }) => {\n    // Test both custom component pages produce valid JSON\n    const pages = [\"/custom-podcast\", \"/custom-service\"];\n\n    for (const pagePath of pages) {\n      await page.goto(pagePath);\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      // Should not throw when parsing\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      // Should have required schema.org properties\n      expect(jsonData).toHaveProperty(\"@context\", \"https://schema.org\");\n      expect(jsonData).toHaveProperty(\"@type\");\n      expect(jsonData).toHaveProperty(\"name\");\n    }\n  });\n\n  test(\"custom components render correctly with partial data\", async ({\n    page,\n  }) => {\n    // The example pages have all data, but this tests that the components\n    // handle conditional properties correctly (all props except name are optional)\n\n    // Both pages should render without errors\n    await page.goto(\"/custom-podcast\");\n    await expect(page.locator(\"h1\")).toContainText(\"Tech Talk Weekly\");\n\n    await page.goto(\"/custom-service\");\n    await expect(page.locator(\"h1\")).toContainText(\"Web Development Services\");\n\n    // Verify no console errors\n    const consoleMessages: string[] = [];\n    page.on(\"console\", (msg) => {\n      if (msg.type() === \"error\") {\n        consoleMessages.push(msg.text());\n      }\n    });\n\n    await page.goto(\"/custom-podcast\");\n    await page.goto(\"/custom-service\");\n\n    expect(consoleMessages).toHaveLength(0);\n  });\n});\n"
  },
  {
    "path": "tests/e2e/datasetJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"DatasetJsonLd\", () => {\n  test(\"renders basic Dataset structured data\", async ({ page }) => {\n    await page.goto(\"/dataset\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Dataset\");\n    expect(jsonData.name).toBe(\"NCDC Storm Events Database\");\n    expect(jsonData.description).toContain(\n      \"Storm Data is provided by the National Weather Service\",\n    );\n    expect(jsonData.url).toBe(\"https://example.com/dataset/storm-events\");\n    expect(jsonData.isAccessibleForFree).toBe(true);\n    expect(jsonData.keywords).toEqual([\n      \"storm\",\n      \"weather\",\n      \"climate\",\n      \"natural disasters\",\n    ]);\n\n    // Verify creator\n    expect(jsonData.creator).toEqual({\n      \"@type\": \"Person\",\n      name: \"NOAA\",\n    });\n\n    // Verify distribution\n    expect(jsonData.distribution).toEqual({\n      \"@type\": \"DataDownload\",\n      contentUrl: \"https://www.ncdc.noaa.gov/stormevents/ftp.jsp\",\n      encodingFormat: \"CSV\",\n    });\n  });\n\n  test(\"renders advanced Dataset with all features\", async ({ page }) => {\n    await page.goto(\"/dataset-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify comprehensive properties\n    expect(jsonData.name).toBe(\"Global Climate Data 2020-2024\");\n    expect(jsonData.sameAs).toEqual([\n      \"https://doi.org/10.1000/182\",\n      \"https://data.gov/dataset/climate-2020-2024\",\n    ]);\n\n    // Verify identifiers\n    expect(jsonData.identifier).toHaveLength(2);\n    expect(jsonData.identifier[0]).toBe(\"https://doi.org/10.1000/182\");\n    expect(jsonData.identifier[1]).toEqual({\n      \"@type\": \"PropertyValue\",\n      value: \"ark:/12345/fk1234\",\n      propertyID: \"ARK\",\n    });\n\n    // Verify license\n    expect(jsonData.license).toEqual({\n      \"@type\": \"CreativeWork\",\n      name: \"Creative Commons Zero v1.0 Universal\",\n      url: \"https://creativecommons.org/publicdomain/zero/1.0/\",\n    });\n\n    // Verify creators array\n    expect(jsonData.creator).toHaveLength(3);\n    expect(jsonData.creator[0][\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.creator[0].name).toBe(\n      \"National Centers for Environmental Information\",\n    );\n    expect(jsonData.creator[0].contactPoint).toEqual({\n      \"@type\": \"ContactPoint\",\n      contactType: \"customer service\",\n      telephone: \"+1-828-271-4800\",\n      email: \"ncei.orders@noaa.gov\",\n    });\n    expect(jsonData.creator[1]).toEqual({\n      \"@type\": \"Person\",\n      name: \"Dr. Jane Smith\",\n    });\n\n    // Verify funder\n    expect(jsonData.funder).toEqual({\n      \"@type\": \"Organization\",\n      name: \"National Science Foundation\",\n      sameAs: \"https://ror.org/021nxhr62\",\n    });\n\n    // Verify included in data catalog\n    expect(jsonData.includedInDataCatalog).toEqual({\n      \"@type\": \"DataCatalog\",\n      name: \"data.gov\",\n      url: \"https://data.gov\",\n    });\n\n    // Verify distributions\n    expect(jsonData.distribution).toHaveLength(3);\n    expect(jsonData.distribution[0]).toEqual({\n      \"@type\": \"DataDownload\",\n      contentUrl: \"https://example.com/data/climate-2020-2024.csv\",\n      encodingFormat: \"CSV\",\n      contentSize: \"2.5GB\",\n      description: \"Complete dataset in CSV format with all measurements\",\n    });\n\n    // Verify temporal and spatial coverage\n    expect(jsonData.temporalCoverage).toBe(\"2020-01-01/2024-12-31\");\n    expect(jsonData.spatialCoverage).toEqual({\n      \"@type\": \"Place\",\n      name: \"Global Coverage\",\n      geo: {\n        \"@type\": \"GeoShape\",\n        box: \"-90 -180 90 180\",\n      },\n    });\n\n    // Verify measurement technique\n    expect(jsonData.measurementTechnique).toEqual([\n      \"Satellite observation\",\n      \"Ground station measurements\",\n      \"Weather balloon data\",\n      \"Ocean buoy sensors\",\n    ]);\n\n    // Verify variable measured\n    expect(jsonData.variableMeasured).toHaveLength(5);\n    expect(jsonData.variableMeasured[0]).toBe(\"temperature\");\n    expect(jsonData.variableMeasured[2]).toEqual({\n      \"@type\": \"PropertyValue\",\n      name: \"Atmospheric Pressure\",\n      value: \"hectopascals\",\n      propertyID: \"PRES\",\n    });\n\n    // Verify other properties\n    expect(jsonData.version).toBe(\"2.1\");\n    expect(jsonData.alternateName).toEqual([\n      \"Global Climate Dataset 2020-2024\",\n      \"GCD-2024\",\n      \"World Climate Data Collection\",\n    ]);\n    expect(jsonData.citation).toHaveLength(2);\n  });\n\n  test(\"renders Dataset with catalog inclusion\", async ({ page }) => {\n    await page.goto(\"/dataset-catalog\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic info\n    expect(jsonData.name).toBe(\"Ocean Temperature Time Series 2023\");\n    expect(jsonData.identifier).toBe(\"https://doi.org/10.5000/ocean-temp-2023\");\n\n    // Verify creator as organization\n    expect(jsonData.creator).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Pacific Ocean Research Institute\",\n      url: \"https://example.com/pori\",\n      logo: \"https://example.com/pori-logo.png\",\n    });\n\n    // Verify data catalog inclusion\n    expect(jsonData.includedInDataCatalog).toEqual({\n      \"@type\": \"DataCatalog\",\n      name: \"Pacific Ocean Climate Data Catalog\",\n      url: \"https://example.com/pacific-climate-catalog\",\n      description: \"Comprehensive collection of Pacific Ocean climate datasets\",\n    });\n\n    // Verify spatial coverage with GeoShape\n    expect(jsonData.spatialCoverage).toEqual({\n      \"@type\": \"Place\",\n      name: \"Pacific Ocean\",\n      geo: {\n        \"@type\": \"GeoShape\",\n        box: \"-60 120 60 -80\",\n      },\n    });\n\n    // Verify variable measured array\n    expect(jsonData.variableMeasured).toHaveLength(2);\n    expect(jsonData.variableMeasured[0]).toEqual({\n      \"@type\": \"PropertyValue\",\n      name: \"Sea Surface Temperature\",\n      value: \"degrees Celsius\",\n      propertyID: \"SST\",\n    });\n  });\n\n  test(\"renders Dataset with nested structure (hasPart)\", async ({ page }) => {\n    await page.goto(\"/dataset-nested\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify parent dataset\n    expect(jsonData.name).toBe(\"World Climate Database 2020-2024\");\n    expect(jsonData[\"@type\"]).toBe(\"Dataset\");\n\n    // Verify hasPart contains sub-datasets\n    expect(jsonData.hasPart).toHaveLength(3);\n\n    // Check first sub-dataset\n    const firstSubDataset = jsonData.hasPart[0];\n    expect(firstSubDataset[\"@type\"]).toBe(\"Dataset\");\n    expect(firstSubDataset.name).toBe(\"North America Climate Data 2020-2024\");\n    expect(firstSubDataset.creator).toEqual({\n      \"@type\": \"Organization\",\n      name: \"North American Weather Service\",\n    });\n    expect(firstSubDataset.distribution).toEqual({\n      \"@type\": \"DataDownload\",\n      contentUrl: \"https://example.com/data/na-climate.csv\",\n      encodingFormat: \"CSV\",\n    });\n    expect(firstSubDataset.spatialCoverage).toEqual({\n      \"@type\": \"Place\",\n      name: \"North America\",\n    });\n\n    // Check second sub-dataset\n    const secondSubDataset = jsonData.hasPart[1];\n    expect(secondSubDataset.name).toBe(\"Europe Climate Data 2020-2024\");\n    expect(secondSubDataset.creator.name).toBe(\"European Climate Agency\");\n\n    // Check third sub-dataset\n    const thirdSubDataset = jsonData.hasPart[2];\n    expect(thirdSubDataset.name).toBe(\"Asia Pacific Climate Data 2020-2024\");\n    expect(thirdSubDataset.spatialCoverage.name).toBe(\"Asia Pacific\");\n\n    // Verify parent dataset properties\n    expect(jsonData.distribution).toEqual({\n      \"@type\": \"DataDownload\",\n      contentUrl: \"https://example.com/data/world-climate-complete.zip\",\n      encodingFormat: \"ZIP\",\n      contentSize: \"12.5GB\",\n      description: \"Complete aggregated dataset with all regional data\",\n    });\n\n    expect(jsonData.spatialCoverage).toBe(\"Global\");\n    expect(jsonData.variableMeasured).toEqual([\n      \"temperature\",\n      \"precipitation\",\n      \"wind speed\",\n      \"humidity\",\n    ]);\n  });\n});\n"
  },
  {
    "path": "tests/e2e/discussionForumPostingJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"DiscussionForumPostingJsonLd\", () => {\n  test(\"renders basic DiscussionForumPosting structured data\", async ({\n    page,\n  }) => {\n    await page.goto(\"/discussion-forum\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"DiscussionForumPosting\");\n    expect(jsonData.headline).toBe(\"I went to the concert!\");\n    expect(jsonData.text).toBe(\"Look at how cool this concert was!\");\n    expect(jsonData.url).toBe(\"https://example.com/forum/very-popular-thread\");\n    expect(jsonData.datePublished).toBe(\"2024-01-01T08:00:00+00:00\");\n\n    // Verify author\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Katie Pope\",\n    });\n\n    // Verify comments\n    expect(jsonData.comment).toHaveLength(2);\n    expect(jsonData.comment[0]).toEqual({\n      \"@type\": \"Comment\",\n      text: \"Who's the person you're with?\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"Saul Douglas\",\n      },\n      datePublished: \"2024-01-01T09:46:02+00:00\",\n    });\n    expect(jsonData.comment[1]).toEqual({\n      \"@type\": \"Comment\",\n      text: \"That's my mom, isn't she cool?\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"Katie Pope\",\n      },\n      datePublished: \"2024-01-01T09:50:25+00:00\",\n    });\n  });\n\n  test(\"renders advanced DiscussionForumPosting with all features\", async ({\n    page,\n  }) => {\n    await page.goto(\"/discussion-forum-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"DiscussionForumPosting\");\n    expect(jsonData.headline).toBe(\"Very Popular Thread About Concerts\");\n\n    // Verify dates\n    expect(jsonData.datePublished).toBe(\"2024-01-01T08:34:34+00:00\");\n    expect(jsonData.dateModified).toBe(\"2024-01-01T09:00:00+00:00\");\n\n    // Verify author with URL\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Katie Pope\",\n      url: \"https://example.com/user/katie-pope\",\n    });\n\n    // Verify images\n    expect(jsonData.image).toHaveLength(2);\n    expect(jsonData.image[0]).toBe(\"https://example.com/concert-photo1.jpg\");\n    expect(jsonData.image[1]).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/concert-photo2.jpg\",\n      width: 1200,\n      height: 800,\n      caption: \"The main stage\",\n    });\n\n    // Verify video\n    expect(jsonData.video).toEqual({\n      \"@type\": \"VideoObject\",\n      name: \"Concert Highlights\",\n      contentUrl: \"https://example.com/concert-video.mp4\",\n      uploadDate: \"2024-01-02T10:00:00+00:00\",\n      thumbnailUrl: \"https://example.com/concert-thumbnail.jpg\",\n      description: \"Best moments from the concert\",\n    });\n\n    // Verify interaction statistics\n    expect(jsonData.interactionStatistic).toHaveLength(3);\n    expect(jsonData.interactionStatistic[0]).toEqual({\n      \"@type\": \"InteractionCounter\",\n      interactionType: \"https://schema.org/LikeAction\",\n      userInteractionCount: 127,\n    });\n\n    // Verify isPartOf\n    expect(jsonData.isPartOf).toEqual({\n      \"@type\": \"CreativeWork\",\n      name: \"Concert Discussions\",\n      url: \"https://example.com/forum/concerts\",\n    });\n\n    // Verify sharedContent\n    expect(jsonData.sharedContent).toEqual({\n      \"@type\": \"WebPage\",\n      url: \"https://example.com/concert-tickets\",\n      name: \"Concert Venue Information\",\n      description: \"Details about the venue and upcoming shows\",\n    });\n\n    // Verify nested comments\n    expect(jsonData.comment).toHaveLength(2);\n    expect(jsonData.comment[0].comment).toHaveLength(1);\n    expect(jsonData.comment[0].comment[0]).toMatchObject({\n      \"@type\": \"Comment\",\n      text: \"Yes it should, it's a great post!\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"Happy Fan\",\n      },\n    });\n\n    // Verify comment with video\n    expect(jsonData.comment[1].video).toMatchObject({\n      \"@type\": \"VideoObject\",\n      name: \"My Concert Video\",\n    });\n  });\n\n  test(\"renders SocialMediaPosting type\", async ({ page }) => {\n    await page.goto(\"/social-media-posting\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify it's SocialMediaPosting type\n    expect(jsonData[\"@type\"]).toBe(\"SocialMediaPosting\");\n\n    // Verify author\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"TechInfluencer\",\n      url: \"https://example.com/user/techinfluencer\",\n    });\n\n    // Verify shared content is processed correctly\n    expect(jsonData.sharedContent).toEqual({\n      \"@type\": \"WebPage\",\n      url: \"https://example.com/ai-tool-review\",\n      name: \"Revolutionary AI Tool for Content Creators\",\n      description:\n        \"A comprehensive review of the latest AI tool that's changing how we create content\",\n    });\n\n    // Verify multiple interaction statistics\n    expect(jsonData.interactionStatistic).toHaveLength(3);\n    const likeAction = jsonData.interactionStatistic.find(\n      (stat: { interactionType: string }) =>\n        stat.interactionType === \"https://schema.org/LikeAction\",\n    );\n    expect(likeAction.userInteractionCount).toBe(342);\n  });\n\n  test(\"renders deleted DiscussionForumPosting\", async ({ page }) => {\n    await page.goto(\"/discussion-forum-deleted\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify deleted status\n    expect(jsonData.creativeWorkStatus).toBe(\"Deleted\");\n    expect(jsonData.headline).toBe(\"[Deleted Post]\");\n    expect(jsonData.text).toBe(\n      \"This post has been removed by the author or moderators.\",\n    );\n\n    // Verify mix of normal and deleted comments\n    expect(jsonData.comment).toHaveLength(3);\n\n    // Check deleted comment\n    expect(jsonData.comment[1].creativeWorkStatus).toBe(\"Deleted\");\n    expect(jsonData.comment[1].text).toBe(\"[This comment has been deleted]\");\n\n    // Check moderator comment (Organization author)\n    expect(jsonData.comment[2].author).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Forum Moderators\",\n      url: \"https://example.com/moderators\",\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e/employerAggregateRatingJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"EmployerAggregateRatingJsonLd\", () => {\n  test(\"renders basic EmployerAggregateRating structured data\", async ({\n    page,\n  }) => {\n    await page.goto(\"/employer-aggregate-rating\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify all properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"EmployerAggregateRating\");\n    expect(jsonData.itemReviewed).toEqual({\n      \"@type\": \"Organization\",\n      name: \"World's Best Coffee Shop\",\n    });\n    expect(jsonData.ratingValue).toBe(91);\n    expect(jsonData.ratingCount).toBe(10561);\n    expect(jsonData.reviewCount).toBeUndefined();\n    expect(jsonData.bestRating).toBeUndefined();\n    expect(jsonData.worstRating).toBeUndefined();\n  });\n\n  test(\"renders advanced EmployerAggregateRating with full Organization details\", async ({\n    page,\n  }) => {\n    await page.goto(\"/employer-aggregate-rating-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"EmployerAggregateRating\");\n\n    // Verify organization details\n    expect(jsonData.itemReviewed[\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.itemReviewed.name).toBe(\"TechCorp International\");\n    expect(jsonData.itemReviewed.sameAs).toBe(\n      \"https://www.techcorp-international.example.com\",\n    );\n    expect(jsonData.itemReviewed.url).toBe(\n      \"https://www.techcorp-international.example.com\",\n    );\n    expect(jsonData.itemReviewed.logo).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/techcorp-logo.png\",\n      width: 600,\n      height: 300,\n    });\n    expect(jsonData.itemReviewed.description).toBe(\n      \"Leading technology company specializing in cloud solutions and AI\",\n    );\n    expect(jsonData.itemReviewed.telephone).toBe(\"+1-555-123-4567\");\n    expect(jsonData.itemReviewed.email).toBe(\"careers@techcorp.example.com\");\n\n    // Verify addresses\n    expect(jsonData.itemReviewed.address).toHaveLength(2);\n    expect(jsonData.itemReviewed.address[0]).toEqual({\n      \"@type\": \"PostalAddress\",\n      streetAddress: \"123 Innovation Way\",\n      addressLocality: \"San Francisco\",\n      addressRegion: \"CA\",\n      postalCode: \"94105\",\n      addressCountry: \"US\",\n    });\n    expect(jsonData.itemReviewed.address[1]).toEqual({\n      \"@type\": \"PostalAddress\",\n      streetAddress: \"456 Tech Park\",\n      addressLocality: \"New York\",\n      addressRegion: \"NY\",\n      postalCode: \"10001\",\n      addressCountry: \"US\",\n    });\n\n    // Verify numberOfEmployees\n    expect(jsonData.itemReviewed.numberOfEmployees).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 5000,\n    });\n\n    // Verify ratings\n    expect(jsonData.ratingValue).toBe(4.7);\n    expect(jsonData.ratingCount).toBe(1842);\n    expect(jsonData.reviewCount).toBe(1755);\n    expect(jsonData.bestRating).toBe(5);\n    expect(jsonData.worstRating).toBe(1);\n  });\n\n  test(\"renders custom scale EmployerAggregateRating with percentage\", async ({\n    page,\n  }) => {\n    await page.goto(\"/employer-aggregate-rating-custom-scale\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"EmployerAggregateRating\");\n\n    // Verify organization\n    expect(jsonData.itemReviewed[\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.itemReviewed.name).toBe(\"Green Energy Corp\");\n    expect(jsonData.itemReviewed.sameAs).toBe(\n      \"https://www.greenenergycorp.example.com\",\n    );\n\n    // Verify percentage rating\n    expect(jsonData.ratingValue).toBe(\"85%\");\n    expect(jsonData.reviewCount).toBe(432);\n    expect(jsonData.ratingCount).toBeUndefined();\n\n    // Verify custom scale\n    expect(jsonData.bestRating).toBe(100);\n    expect(jsonData.worstRating).toBe(0);\n  });\n\n  test(\"page content matches structured data\", async ({ page }) => {\n    await page.goto(\"/employer-aggregate-rating\");\n\n    // Check visible content\n    await expect(\n      page.getByRole(\"heading\", {\n        name: \"World's Best Coffee Shop - Employer Ratings\",\n      }),\n    ).toBeVisible();\n    await expect(page.getByText(\"91\")).toBeVisible();\n    await expect(page.getByText(\"out of 100\")).toBeVisible();\n    await expect(\n      page.getByText(\"Based on 10,561 employee ratings\"),\n    ).toBeVisible();\n\n    // Get structured data\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify structured data matches visible content\n    expect(jsonData.itemReviewed.name).toBe(\"World's Best Coffee Shop\");\n    expect(jsonData.ratingValue).toBe(91);\n    expect(jsonData.ratingCount).toBe(10561);\n  });\n\n  test(\"validates JSON-LD format is parseable\", async ({ page }) => {\n    await page.goto(\"/employer-aggregate-rating-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n\n    // Should not throw\n    expect(() => JSON.parse(jsonLdScript!)).not.toThrow();\n\n    // Verify it's valid JSON-LD\n    const jsonData = JSON.parse(jsonLdScript!);\n    expect(jsonData[\"@context\"]).toBeDefined();\n    expect(jsonData[\"@type\"]).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "tests/e2e/eventJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"EventJsonLd\", () => {\n  test(\"renders basic Event structured data\", async ({ page }) => {\n    await page.goto(\"/event\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Event\");\n    expect(jsonData.name).toBe(\"The Adventures of Kira and Morrison\");\n    expect(jsonData.startDate).toBe(\"2025-07-21T19:00-05:00\");\n    expect(jsonData.endDate).toBe(\"2025-07-21T23:00-05:00\");\n\n    // Verify location\n    expect(jsonData.location).toEqual({\n      \"@type\": \"Place\",\n      name: \"Snickerpark Stadium\",\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"100 West Snickerpark Dr\",\n        addressLocality: \"Snickertown\",\n        postalCode: \"19019\",\n        addressRegion: \"PA\",\n        addressCountry: \"US\",\n      },\n    });\n\n    // Verify images array\n    expect(jsonData.image).toEqual([\n      \"https://example.com/photos/1x1/photo.jpg\",\n      \"https://example.com/photos/4x3/photo.jpg\",\n      \"https://example.com/photos/16x9/photo.jpg\",\n    ]);\n\n    // Verify offers\n    expect(jsonData.offers).toEqual({\n      \"@type\": \"Offer\",\n      url: \"https://www.example.com/event_offer/12345_202403180430\",\n      price: 30,\n      priceCurrency: \"USD\",\n      availability: \"https://schema.org/InStock\",\n      validFrom: \"2024-05-21T12:00\",\n    });\n\n    // Verify performer\n    expect(jsonData.performer).toEqual({\n      \"@type\": \"PerformingGroup\",\n      name: \"Kira and Morrison\",\n    });\n\n    // Verify organizer\n    expect(jsonData.organizer).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Kira and Morrison Music\",\n      url: \"https://kiraandmorrisonmusic.com\",\n    });\n\n    // Verify description\n    expect(jsonData.description).toBe(\n      \"The Adventures of Kira and Morrison is coming to Snickertown in a can't miss performance.\",\n    );\n  });\n\n  test(\"renders cancelled Event structured data\", async ({ page }) => {\n    await page.goto(\"/event-cancelled\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Event\");\n    expect(jsonData.name).toBe(\"Summer Music Festival 2025\");\n    expect(jsonData.eventStatus).toBe(\"https://schema.org/EventCancelled\");\n\n    // Verify dates are preserved for cancelled events\n    expect(jsonData.startDate).toBe(\"2025-08-15T12:00:00-05:00\");\n    expect(jsonData.endDate).toBe(\"2025-08-17T23:00:00-05:00\");\n\n    // Verify location is preserved\n    expect(jsonData.location.name).toBe(\"City Park Amphitheater\");\n    expect(jsonData.location.address.addressLocality).toBe(\"Austin\");\n\n    // Verify offers show as sold out\n    expect(jsonData.offers.availability).toBe(\"https://schema.org/SoldOut\");\n  });\n\n  test(\"renders rescheduled Event structured data\", async ({ page }) => {\n    await page.goto(\"/event-rescheduled\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Event\");\n    expect(jsonData.name).toBe(\"Tech Conference 2025: Future of AI\");\n    expect(jsonData.eventStatus).toBe(\"https://schema.org/EventRescheduled\");\n\n    // Verify new dates\n    expect(jsonData.startDate).toBe(\"2025-09-20T09:00:00-07:00\");\n    expect(jsonData.endDate).toBe(\"2025-09-22T17:00:00-07:00\");\n\n    // Verify previous dates\n    expect(jsonData.previousStartDate).toEqual([\n      \"2025-03-15T09:00:00-07:00\",\n      \"2025-06-10T09:00:00-07:00\",\n    ]);\n\n    // Verify multiple offers\n    expect(jsonData.offers).toHaveLength(2);\n    expect(jsonData.offers[0].price).toBe(299);\n    expect(jsonData.offers[1].price).toBe(599);\n\n    // Verify multiple performers\n    expect(jsonData.performer).toHaveLength(3);\n    expect(jsonData.performer[0][\"@type\"]).toBe(\"Person\");\n    expect(jsonData.performer[0].name).toBe(\"Dr. Sarah Chen\");\n    expect(jsonData.performer[2]).toEqual({\n      \"@type\": \"PerformingGroup\",\n      name: \"Panel of Industry Experts\",\n    });\n\n    // Verify URL\n    expect(jsonData.url).toBe(\"https://techconf2025.com\");\n  });\n\n  test(\"renders free Event structured data\", async ({ page }) => {\n    await page.goto(\"/event-free\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Event\");\n    expect(jsonData.name).toBe(\n      \"Community Coding Workshop: Introduction to Web Development\",\n    );\n\n    // Verify free event has price 0\n    expect(jsonData.offers.price).toBe(0);\n    expect(jsonData.offers.priceCurrency).toBe(\"USD\");\n    expect(jsonData.offers.availability).toBe(\"https://schema.org/InStock\");\n\n    // Verify string performer is converted\n    expect(jsonData.performer).toEqual({\n      \"@type\": \"PerformingGroup\",\n      name: \"Sarah Johnson\",\n    });\n\n    // Verify location\n    expect(jsonData.location.name).toBe(\"Downtown Public Library\");\n    expect(jsonData.location.address.addressLocality).toBe(\"Springfield\");\n    expect(jsonData.location.address.addressRegion).toBe(\"IL\");\n  });\n\n  test(\"supports custom script ID\", async ({ page }) => {\n    // Create a test page with custom script ID\n    await page.evaluate(() => {\n      const script = document.createElement(\"script\");\n      script.type = \"application/ld+json\";\n      script.id = \"event-custom-id\";\n      script.textContent = JSON.stringify({\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Event\",\n        name: \"Test Event\",\n        startDate: \"2025-01-01T00:00:00\",\n        location: {\n          \"@type\": \"Place\",\n          address: { \"@type\": \"PostalAddress\", name: \"Test\" },\n        },\n      });\n      document.head.appendChild(script);\n    });\n\n    const scriptWithId = await page.locator(\"#event-custom-id\");\n    expect(await scriptWithId.count()).toBe(1);\n  });\n});\n"
  },
  {
    "path": "tests/e2e/faqJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"FAQJsonLd\", () => {\n  test(\"renders basic FAQ structured data\", async ({ page }) => {\n    await page.goto(\"/faq\");\n\n    // Find the JSON-LD script tag\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify FAQPage structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"FAQPage\");\n    expect(jsonData.mainEntity).toHaveLength(4);\n\n    // Verify first question\n    const firstQuestion = jsonData.mainEntity[0];\n    expect(firstQuestion[\"@type\"]).toBe(\"Question\");\n    expect(firstQuestion.name).toBe(\"How to find an apprenticeship?\");\n    expect(firstQuestion.acceptedAnswer[\"@type\"]).toBe(\"Answer\");\n    expect(firstQuestion.acceptedAnswer.text).toContain(\n      \"We provide an official service to search through available apprenticeships\",\n    );\n\n    // Verify second question\n    const secondQuestion = jsonData.mainEntity[1];\n    expect(secondQuestion.name).toBe(\"Whom to contact?\");\n    expect(secondQuestion.acceptedAnswer.text).toContain(\n      \"You can contact the apprenticeship office\",\n    );\n\n    // Verify all questions have proper structure\n    jsonData.mainEntity.forEach(\n      (question: {\n        \"@type\": string;\n        name: string;\n        acceptedAnswer: { \"@type\": string; text: string };\n      }) => {\n        expect(question[\"@type\"]).toBe(\"Question\");\n        expect(question.name).toBeTruthy();\n        expect(question.acceptedAnswer).toBeTruthy();\n        expect(question.acceptedAnswer[\"@type\"]).toBe(\"Answer\");\n        expect(question.acceptedAnswer.text).toBeTruthy();\n      },\n    );\n  });\n\n  test(\"renders FAQ with HTML content in answers\", async ({ page }) => {\n    await page.goto(\"/faq-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"FAQPage\");\n    expect(jsonData.mainEntity).toHaveLength(4);\n\n    // Check HTML content is preserved\n    const firstQuestion = jsonData.mainEntity[0];\n    expect(firstQuestion.name).toBe(\n      \"What documents are required for application?\",\n    );\n    expect(firstQuestion.acceptedAnswer.text).toContain(\"<p>\");\n    expect(firstQuestion.acceptedAnswer.text).toContain(\"<ul>\");\n    expect(firstQuestion.acceptedAnswer.text).toContain(\"<li>\");\n    expect(firstQuestion.acceptedAnswer.text).toContain(\n      '<a href=\"/forms/medical\">',\n    );\n\n    // Check second question with ordered list\n    const secondQuestion = jsonData.mainEntity[1];\n    expect(secondQuestion.acceptedAnswer.text).toContain(\"<ol>\");\n    expect(secondQuestion.acceptedAnswer.text).toContain(\"<strong>\");\n\n    // Check third question with headers\n    const thirdQuestion = jsonData.mainEntity[2];\n    expect(thirdQuestion.acceptedAnswer.text).toContain(\"<h3>\");\n    expect(thirdQuestion.acceptedAnswer.text).toContain(\"<div>\");\n\n    // Check Schema.org format with Answer object\n    const fourthQuestion = jsonData.mainEntity[3];\n    expect(fourthQuestion.name).toBe(\n      \"Are there any special requirements for international students?\",\n    );\n    expect(fourthQuestion.acceptedAnswer[\"@type\"]).toBe(\"Answer\");\n    expect(fourthQuestion.acceptedAnswer.text).toContain(\"<strong>\");\n  });\n\n  test(\"verifies custom script attributes\", async ({ page }) => {\n    await page.goto(\"/faq-advanced\");\n\n    // Check for custom scriptId\n    const scriptById = await page.locator(\"#advanced-faq-jsonld\");\n    expect(await scriptById.count()).toBe(1);\n    expect(await scriptById.getAttribute(\"type\")).toBe(\"application/ld+json\");\n\n    // Check for data-testid (which is set to the same value as scriptId)\n    const scriptByTestId = await page.locator(\n      '[data-testid=\"advanced-faq-jsonld\"]',\n    );\n    expect(await scriptByTestId.count()).toBe(1);\n  });\n\n  test(\"renders health-focused FAQ example\", async ({ page }) => {\n    await page.goto(\"/faq-health\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"FAQPage\");\n    expect(jsonData.mainEntity).toHaveLength(5);\n\n    // Verify health-specific content\n    const covidQuestion = jsonData.mainEntity[0];\n    expect(covidQuestion.name).toBe(\"What are the symptoms of COVID-19?\");\n    expect(covidQuestion.acceptedAnswer.text).toContain(\"<ul>\");\n    expect(covidQuestion.acceptedAnswer.text).toContain(\"Fever or chills\");\n\n    // Verify vaccination scheduling question\n    const vaccineQuestion = jsonData.mainEntity[1];\n    expect(vaccineQuestion.name).toBe(\n      \"How do I schedule a vaccination appointment?\",\n    );\n    expect(vaccineQuestion.acceptedAnswer.text).toContain(\"1-800-VACCINE\");\n\n    // Verify all questions maintain proper structure\n    jsonData.mainEntity.forEach(\n      (question: { \"@type\": string; acceptedAnswer: { \"@type\": string } }) => {\n        expect(question[\"@type\"]).toBe(\"Question\");\n        expect(question.acceptedAnswer[\"@type\"]).toBe(\"Answer\");\n      },\n    );\n  });\n\n  test(\"FAQ content matches visible page content\", async ({ page }) => {\n    await page.goto(\"/faq\");\n\n    // Get structured data\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify first question appears on page\n    const firstQuestionText = await page\n      .locator(\"h2\", { hasText: \"How to find an apprenticeship?\" })\n      .textContent();\n    expect(firstQuestionText).toBeTruthy();\n\n    // Verify answer text appears on page\n    const answerText = await page\n      .locator(\"p\", {\n        hasText: \"We provide an official service to search through available\",\n      })\n      .first()\n      .textContent();\n    expect(answerText).toBeTruthy();\n\n    // Verify structured data matches visible content\n    expect(jsonData.mainEntity[0].name).toBe(\"How to find an apprenticeship?\");\n    expect(jsonData.mainEntity[0].acceptedAnswer.text).toContain(\n      \"We provide an official service\",\n    );\n  });\n});\n"
  },
  {
    "path": "tests/e2e/howtoJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"HowToJsonLd\", () => {\n  test(\"renders basic HowTo structured data\", async ({ page }) => {\n    await page.goto(\"/howto\");\n\n    // Find the JSON-LD script tag\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic HowTo properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"HowTo\");\n    expect(jsonData.name).toBe(\"How to Change a Flat Tire\");\n    expect(jsonData.description).toBe(\n      \"Step-by-step guide to safely change a flat tire on the roadside\",\n    );\n    expect(jsonData.image).toBe(\"https://example.com/images/tire-change.jpg\");\n    expect(jsonData.estimatedCost).toBe(\"$20\");\n    expect(jsonData.prepTime).toBe(\"PT5M\");\n    expect(jsonData.performTime).toBe(\"PT25M\");\n    expect(jsonData.totalTime).toBe(\"PT30M\");\n    expect(jsonData.yield).toBe(\"1 changed tire\");\n\n    // Verify tools array\n    expect(jsonData.tool).toHaveLength(4);\n    expect(jsonData.tool[0]).toEqual({\n      \"@type\": \"HowToTool\",\n      name: \"Spare tire\",\n    });\n    expect(jsonData.tool[1]).toEqual({\n      \"@type\": \"HowToTool\",\n      name: \"Lug wrench\",\n    });\n    expect(jsonData.tool[2]).toEqual({\n      \"@type\": \"HowToTool\",\n      name: \"Jack\",\n    });\n    expect(jsonData.tool[3]).toEqual({\n      \"@type\": \"HowToTool\",\n      name: \"Wheel wedges\",\n    });\n\n    // Verify supplies array\n    expect(jsonData.supply).toHaveLength(1);\n    expect(jsonData.supply[0]).toEqual({\n      \"@type\": \"HowToSupply\",\n      name: \"Flares\",\n    });\n\n    // Verify steps array\n    expect(jsonData.step).toHaveLength(9);\n    expect(jsonData.step[0]).toBe(\n      \"Turn on your hazard lights and apply parking brake\",\n    );\n    expect(jsonData.step[8]).toBe(\n      \"Check the tire pressure and drive to a service station\",\n    );\n  });\n\n  test(\"renders advanced HowTo with sections and detailed steps\", async ({\n    page,\n  }) => {\n    await page.goto(\"/howto-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify HowTo type and name\n    expect(jsonData[\"@type\"]).toBe(\"HowTo\");\n    expect(jsonData.name).toBe(\"How to Change a Flat Tire\");\n\n    // Verify ImageObject\n    expect(jsonData.image).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/images/tire-change-guide.jpg\",\n      width: 1200,\n      height: 800,\n    });\n\n    // Verify MonetaryAmount estimated cost\n    expect(jsonData.estimatedCost).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      currency: \"USD\",\n      value: 20,\n    });\n\n    // Verify durations\n    expect(jsonData.prepTime).toBe(\"PT5M\");\n    expect(jsonData.performTime).toBe(\"PT25M\");\n    expect(jsonData.totalTime).toBe(\"PT30M\");\n\n    // Verify tools with images\n    expect(jsonData.tool).toHaveLength(4);\n    expect(jsonData.tool[1]).toEqual({\n      \"@type\": \"HowToTool\",\n      name: \"Lug wrench\",\n      image: \"https://example.com/images/lug-wrench.jpg\",\n    });\n    expect(jsonData.tool[3]).toEqual({\n      \"@type\": \"HowToTool\",\n      name: \"Wheel wedges\",\n      image: \"https://example.com/images/wheel-wedges.jpg\",\n    });\n\n    // Verify supply with image\n    expect(jsonData.supply).toHaveLength(1);\n    expect(jsonData.supply[0]).toEqual({\n      \"@type\": \"HowToSupply\",\n      name: \"Flares\",\n      image: \"https://example.com/images/flares.jpg\",\n    });\n\n    // Verify HowToSections\n    expect(jsonData.step).toHaveLength(3);\n\n    // First section: Preparation\n    expect(jsonData.step[0][\"@type\"]).toBe(\"HowToSection\");\n    expect(jsonData.step[0].name).toBe(\"Preparation\");\n    expect(jsonData.step[0].position).toBe(1);\n    expect(jsonData.step[0].itemListElement).toHaveLength(2);\n\n    // Check HowToDirection and HowToTip in first step\n    const firstStep = jsonData.step[0].itemListElement[0];\n    expect(firstStep[\"@type\"]).toBe(\"HowToStep\");\n    expect(firstStep.itemListElement).toHaveLength(2);\n    expect(firstStep.itemListElement[0][\"@type\"]).toBe(\"HowToDirection\");\n    expect(firstStep.itemListElement[0].text).toBe(\n      \"Turn on your hazard lights and set the flares.\",\n    );\n    expect(firstStep.itemListElement[1][\"@type\"]).toBe(\"HowToTip\");\n    expect(firstStep.itemListElement[1].text).toBe(\n      \"You're going to need space and want to be visible.\",\n    );\n\n    // Second section: Raise the car\n    expect(jsonData.step[1][\"@type\"]).toBe(\"HowToSection\");\n    expect(jsonData.step[1].name).toBe(\"Raise the car\");\n    expect(jsonData.step[1].position).toBe(2);\n    expect(jsonData.step[1].itemListElement).toHaveLength(5);\n\n    // Check step with image\n    expect(jsonData.step[1].itemListElement[0].image).toBe(\n      \"https://example.com/images/position-jack.jpg\",\n    );\n\n    // Check step with beforeMedia and afterMedia\n    const raiseStep = jsonData.step[1].itemListElement[1];\n    expect(raiseStep.itemListElement[0].beforeMedia).toBe(\n      \"https://example.com/images/car-on-ground.jpg\",\n    );\n    expect(raiseStep.itemListElement[0].afterMedia).toBe(\n      \"https://example.com/images/car-raised.jpg\",\n    );\n\n    // Third section: Finishing up\n    expect(jsonData.step[2][\"@type\"]).toBe(\"HowToSection\");\n    expect(jsonData.step[2].name).toBe(\"Finishing up\");\n    expect(jsonData.step[2].position).toBe(3);\n    expect(jsonData.step[2].itemListElement).toHaveLength(3);\n\n    // Verify video object\n    expect(jsonData.video).toEqual({\n      \"@type\": \"VideoObject\",\n      name: \"How to Change a Tire Video Tutorial\",\n      description:\n        \"Watch our mechanic demonstrate the proper technique for changing a flat tire\",\n      thumbnailUrl: \"https://example.com/video/tire-change-thumb.jpg\",\n      contentUrl: \"https://example.com/video/tire-change-tutorial.mp4\",\n      uploadDate: \"2024-01-15T08:00:00+00:00\",\n      duration: \"PT8M30S\",\n    });\n  });\n\n  test(\"verifies all required properties are present\", async ({ page }) => {\n    await page.goto(\"/howto\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify required properties according to Schema.org documentation\n    expect(jsonData).toHaveProperty(\"@context\");\n    expect(jsonData).toHaveProperty(\"@type\");\n    expect(jsonData).toHaveProperty(\"name\");\n\n    // Verify the values are not empty\n    expect(jsonData[\"@context\"]).toBeTruthy();\n    expect(jsonData[\"@type\"]).toBeTruthy();\n    expect(jsonData.name).toBeTruthy();\n  });\n\n  test(\"verifies ISO 8601 duration format for time properties\", async ({\n    page,\n  }) => {\n    await page.goto(\"/howto\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify ISO 8601 duration format\n    expect(jsonData.prepTime).toMatch(/^PT\\d+[HMS]/);\n    expect(jsonData.performTime).toMatch(/^PT\\d+[HMS]/);\n    expect(jsonData.totalTime).toMatch(/^PT\\d+[HMS]/);\n\n    // Verify specific values\n    expect(jsonData.prepTime).toBe(\"PT5M\");\n    expect(jsonData.performTime).toBe(\"PT25M\");\n    expect(jsonData.totalTime).toBe(\"PT30M\");\n  });\n\n  test(\"verifies correct @type values for nested objects\", async ({ page }) => {\n    await page.goto(\"/howto-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify tool @type\n    for (const tool of jsonData.tool) {\n      expect(tool[\"@type\"]).toBe(\"HowToTool\");\n    }\n\n    // Verify supply @type\n    for (const supply of jsonData.supply) {\n      expect(supply[\"@type\"]).toBe(\"HowToSupply\");\n    }\n\n    // Verify section @type\n    for (const section of jsonData.step) {\n      expect(section[\"@type\"]).toBe(\"HowToSection\");\n    }\n\n    // Verify video @type\n    expect(jsonData.video[\"@type\"]).toBe(\"VideoObject\");\n\n    // Verify image @type\n    expect(jsonData.image[\"@type\"]).toBe(\"ImageObject\");\n\n    // Verify estimatedCost @type\n    expect(jsonData.estimatedCost[\"@type\"]).toBe(\"MonetaryAmount\");\n  });\n});\n"
  },
  {
    "path": "tests/e2e/imageJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"ImageJsonLd\", () => {\n  test(\"renders basic Image structured data\", async ({ page }) => {\n    await page.goto(\"/image\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify all properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"ImageObject\");\n    expect(jsonData.contentUrl).toBe(\n      \"https://example.com/photos/black-labrador-puppy.jpg\",\n    );\n\n    // Verify creator was processed to Person\n    expect(jsonData.creator).toEqual({\n      \"@type\": \"Person\",\n      name: \"Brixton Brownstone\",\n    });\n\n    expect(jsonData.creditText).toBe(\"Labrador PhotoLab\");\n    expect(jsonData.copyrightNotice).toBe(\"Clara Kent\");\n    expect(jsonData.license).toBe(\"https://example.com/license\");\n    expect(jsonData.acquireLicensePage).toBe(\n      \"https://example.com/how-to-use-my-images\",\n    );\n  });\n\n  test(\"renders advanced Image with Organization creator\", async ({ page }) => {\n    await page.goto(\"/image-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"ImageObject\");\n    expect(jsonData.contentUrl).toBe(\n      \"https://example.com/photos/sunset-landscape.jpg\",\n    );\n\n    // Verify Organization creator with nested properties\n    expect(jsonData.creator).toEqual({\n      \"@type\": \"Organization\",\n      name: \"PhotoLab Studios\",\n      logo: \"https://example.com/photolab-logo.jpg\",\n      sameAs: [\n        \"https://twitter.com/photolab\",\n        \"https://instagram.com/photolab\",\n        \"https://facebook.com/photolab\",\n      ],\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"123 Photography Lane\",\n        addressLocality: \"San Francisco\",\n        addressRegion: \"CA\",\n        postalCode: \"94105\",\n        addressCountry: \"US\",\n      },\n    });\n\n    expect(jsonData.license).toBe(\n      \"https://creativecommons.org/licenses/by-nc/4.0/\",\n    );\n    expect(jsonData.acquireLicensePage).toBe(\n      \"https://example.com/licensing/premium\",\n    );\n    expect(jsonData.creditText).toBe(\n      \"PhotoLab Studios - Professional Photography\",\n    );\n    expect(jsonData.copyrightNotice).toBe(\n      \"© 2024 PhotoLab Studios. All rights reserved.\",\n    );\n  });\n\n  test(\"renders multiple images with @graph\", async ({ page }) => {\n    await page.goto(\"/image-multiple\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@graph\"]).toBeDefined();\n    expect(Array.isArray(jsonData[\"@graph\"])).toBe(true);\n    expect(jsonData[\"@graph\"]).toHaveLength(3);\n\n    // Verify first image\n    const firstImage = jsonData[\"@graph\"][0];\n    expect(firstImage[\"@type\"]).toBe(\"ImageObject\");\n    expect(firstImage.contentUrl).toBe(\n      \"https://example.com/photos/mountain-sunrise.jpg\",\n    );\n    expect(firstImage.creator).toEqual({\n      \"@type\": \"Person\",\n      name: \"Alex Mountain\",\n    });\n    expect(firstImage.license).toBe(\"https://example.com/license/standard\");\n    expect(firstImage.creditText).toBe(\"Nature Photography Collection\");\n    expect(firstImage.copyrightNotice).toBe(\"© 2024 Alex Mountain\");\n\n    // Verify second image with multiple creators\n    const secondImage = jsonData[\"@graph\"][1];\n    expect(secondImage[\"@type\"]).toBe(\"ImageObject\");\n    expect(secondImage.contentUrl).toBe(\n      \"https://example.com/photos/ocean-waves.jpg\",\n    );\n    expect(Array.isArray(secondImage.creator)).toBe(true);\n    expect(secondImage.creator).toHaveLength(2);\n    expect(secondImage.creator[0]).toEqual({\n      \"@type\": \"Person\",\n      name: \"Sarah Ocean\",\n    });\n    expect(secondImage.creator[1]).toEqual({\n      \"@type\": \"Person\",\n      name: \"Coastal Studios\",\n      url: \"https://coastalstudios.com\",\n    });\n    expect(secondImage.license).toBe(\n      \"https://creativecommons.org/licenses/by-sa/4.0/\",\n    );\n    expect(secondImage.acquireLicensePage).toBe(\n      \"https://example.com/licensing/ocean-collection\",\n    );\n\n    // Verify third image with Organization creator\n    const thirdImage = jsonData[\"@graph\"][2];\n    expect(thirdImage[\"@type\"]).toBe(\"ImageObject\");\n    expect(thirdImage.contentUrl).toBe(\n      \"https://example.com/photos/city-lights.jpg\",\n    );\n    expect(thirdImage.creator).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Urban Photography Inc.\",\n      logo: \"https://example.com/urban-photo-logo.jpg\",\n      sameAs: [\"https://instagram.com/urbanphoto\"],\n    });\n    expect(thirdImage.license).toBe(\"https://example.com/license/commercial\");\n    expect(thirdImage.acquireLicensePage).toBe(\n      \"https://example.com/licensing/urban\",\n    );\n  });\n});\n"
  },
  {
    "path": "tests/e2e/jobPostingJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"JobPostingJsonLd\", () => {\n  test(\"renders basic JobPosting structured data\", async ({ page }) => {\n    await page.goto(\"/job-posting\");\n\n    // Find the JSON-LD script tag\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic JobPosting properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"JobPosting\");\n    expect(jsonData.title).toBe(\"Software Engineer\");\n    expect(jsonData.description).toContain(\n      \"We are looking for a passionate Software Engineer\",\n    );\n    expect(jsonData.datePosted).toBe(\"2024-01-18\");\n    expect(jsonData.validThrough).toBe(\"2024-03-18T00:00\");\n\n    // Verify hiring organization\n    expect(jsonData.hiringOrganization).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Tech Solutions Inc.\",\n    });\n\n    // Verify job location\n    expect(jsonData.jobLocation).toEqual({\n      \"@type\": \"Place\",\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"San Francisco, CA\",\n      },\n    });\n\n    // Verify employment type\n    expect(jsonData.employmentType).toBe(\"FULL_TIME\");\n\n    // Verify salary\n    expect(jsonData.baseSalary).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      currency: \"USD\",\n      value: {\n        \"@type\": \"QuantitativeValue\",\n        minValue: 90000,\n        maxValue: 120000,\n        unitText: \"YEAR\",\n      },\n    });\n  });\n\n  test(\"renders remote JobPosting with applicant location requirements\", async ({\n    page,\n  }) => {\n    await page.goto(\"/job-posting-remote\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify remote job properties\n    expect(jsonData[\"@type\"]).toBe(\"JobPosting\");\n    expect(jsonData.title).toBe(\"Senior Frontend Developer\");\n    expect(jsonData.jobLocationType).toBe(\"TELECOMMUTE\");\n\n    // Verify applicant location requirements\n    expect(jsonData.applicantLocationRequirements).toEqual({\n      \"@type\": \"Country\",\n      name: \"USA\",\n    });\n\n    // Verify hiring organization with full details\n    expect(jsonData.hiringOrganization).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Remote First Tech\",\n      sameAs: \"https://www.remotefirsttech.com\",\n      logo: \"https://www.remotefirsttech.com/logo.png\",\n    });\n\n    // Verify other properties\n    expect(jsonData.url).toBe(\n      \"https://careers.remotefirsttech.com/jobs/senior-frontend-dev\",\n    );\n    expect(jsonData.identifier).toEqual({\n      \"@type\": \"PropertyValue\",\n      value: \"RFT-2024-001\",\n    });\n    expect(jsonData.directApply).toBe(true);\n\n    // Verify education and experience\n    expect(jsonData.educationRequirements).toBe(\"bachelor degree\");\n    expect(jsonData.experienceRequirements).toEqual({\n      \"@type\": \"OccupationalExperienceRequirements\",\n      monthsOfExperience: 60,\n    });\n  });\n\n  test(\"renders advanced JobPosting with multiple locations and all features\", async ({\n    page,\n  }) => {\n    await page.goto(\"/job-posting-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify advanced properties\n    expect(jsonData[\"@type\"]).toBe(\"JobPosting\");\n    expect(jsonData.title).toBe(\"Senior Product Manager\");\n\n    // Verify multiple job locations\n    expect(jsonData.jobLocation).toHaveLength(2);\n    expect(jsonData.jobLocation[0]).toEqual({\n      \"@type\": \"Place\",\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"1600 Amphitheatre Parkway\",\n        addressLocality: \"Mountain View\",\n        addressRegion: \"CA\",\n        postalCode: \"94043\",\n        addressCountry: \"US\",\n      },\n    });\n    expect(jsonData.jobLocation[1]).toEqual({\n      \"@type\": \"Place\",\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"111 8th Avenue\",\n        addressLocality: \"New York\",\n        addressRegion: \"NY\",\n        postalCode: \"10011\",\n        addressCountry: \"US\",\n      },\n    });\n\n    // Verify hybrid/remote setup\n    expect(jsonData.jobLocationType).toBe(\"TELECOMMUTE\");\n    expect(jsonData.applicantLocationRequirements).toHaveLength(4);\n    expect(jsonData.applicantLocationRequirements[0]).toEqual({\n      \"@type\": \"State\",\n      name: \"California, USA\",\n    });\n    expect(jsonData.applicantLocationRequirements[3]).toEqual({\n      \"@type\": \"State\",\n      name: \"Texas, USA\",\n    });\n\n    // Verify multiple employment types\n    expect(jsonData.employmentType).toEqual([\"FULL_TIME\", \"CONTRACTOR\"]);\n\n    // Verify identifier object\n    expect(jsonData.identifier).toEqual({\n      \"@type\": \"PropertyValue\",\n      name: \"Google\",\n      value: \"GCP-PM-2024-001\",\n    });\n\n    // Verify hiring organization with logo object\n    expect(jsonData.hiringOrganization).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Google\",\n      sameAs: \"https://www.google.com\",\n      logo: {\n        \"@type\": \"ImageObject\",\n        url: \"https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png\",\n        width: 272,\n        height: 92,\n      },\n    });\n\n    // Verify salary range\n    expect(jsonData.baseSalary).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      currency: \"USD\",\n      value: {\n        \"@type\": \"QuantitativeValue\",\n        minValue: 180000,\n        maxValue: 280000,\n        unitText: \"YEAR\",\n      },\n    });\n\n    // Verify education requirements array\n    expect(jsonData.educationRequirements).toHaveLength(2);\n    expect(jsonData.educationRequirements[0]).toEqual({\n      \"@type\": \"EducationalOccupationalCredential\",\n      credentialCategory: \"bachelor degree\",\n    });\n    expect(jsonData.educationRequirements[1]).toEqual({\n      \"@type\": \"EducationalOccupationalCredential\",\n      credentialCategory: \"postgraduate degree\",\n    });\n\n    // Verify experience requirements\n    expect(jsonData.experienceRequirements).toEqual({\n      \"@type\": \"OccupationalExperienceRequirements\",\n      monthsOfExperience: 84,\n    });\n\n    // Verify experience can replace education\n    expect(jsonData.experienceInPlaceOfEducation).toBe(true);\n\n    // Verify URL and dates\n    expect(jsonData.url).toBe(\n      \"https://careers.google.com/jobs/senior-product-manager-cloud\",\n    );\n    expect(jsonData.datePosted).toBe(\"2024-01-18\");\n    expect(jsonData.validThrough).toBe(\"2024-04-18T23:59:59\");\n    expect(jsonData.directApply).toBe(true);\n  });\n\n  test(\"correctly identifies State vs Country in applicant location requirements\", async ({\n    page,\n  }) => {\n    await page.goto(\"/job-posting-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify state detection based on format\n    const locations = jsonData.applicantLocationRequirements;\n\n    // Should detect states (contains comma and state name)\n    expect(locations[0][\"@type\"]).toBe(\"State\");\n    expect(locations[0].name).toBe(\"California, USA\");\n\n    expect(locations[1][\"@type\"]).toBe(\"State\");\n    expect(locations[1].name).toBe(\"New York, USA\");\n\n    // Also check remote job with country\n    await page.goto(\"/job-posting-remote\");\n    const remoteJsonLd = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const remoteData = JSON.parse(remoteJsonLd!);\n\n    // Should detect country (no comma, just country name)\n    expect(remoteData.applicantLocationRequirements[\"@type\"]).toBe(\"Country\");\n    expect(remoteData.applicantLocationRequirements.name).toBe(\"USA\");\n  });\n\n  test(\"verifies HTML content is properly structured in description\", async ({\n    page,\n  }) => {\n    await page.goto(\"/job-posting-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify HTML tags are preserved in description\n    expect(jsonData.description).toContain(\"<p>\");\n    expect(jsonData.description).toContain(\"<strong>\");\n    expect(jsonData.description).toContain(\"<ul>\");\n    expect(jsonData.description).toContain(\"<li>\");\n    expect(jsonData.description).toContain(\n      \"7+ years of product management experience\",\n    );\n  });\n});\n"
  },
  {
    "path": "tests/e2e/jsonLdScript.e2e.spec.ts",
    "content": "// tests/e2e/jsonLdScript.e2e.spec.ts\nimport { test, expect } from \"@playwright/test\";\n\nconst PAGE_URL = \"/jsonld-test-page\"; // The path to your test page in the example app\nconst SCRIPT_SELECTOR = 'script[type=\"application/ld+json\"]#e2e-jsonld-script'; // More specific selector\n\ntest.describe(\"JsonLdScript E2E Test\", () => {\n  test(\"should render the JSON-LD script tag with correct content\", async ({\n    page,\n  }) => {\n    // 1. Navigate to the page\n    await page.goto(PAGE_URL);\n\n    // 2. Locate the script tag\n    const scriptHandle = await page.waitForSelector(SCRIPT_SELECTOR, {\n      state: \"attached\",\n    });\n    expect(scriptHandle).toBeTruthy();\n\n    // 3. Extract its content\n    const scriptContent = await scriptHandle.innerHTML();\n    expect(scriptContent).toBeTruthy();\n\n    // 4. Perform assertions\n    let jsonData;\n    try {\n      jsonData = JSON.parse(scriptContent);\n    } catch (e) {\n      // Fail the test if JSON is invalid\n      expect(e, \"Script content should be valid JSON\").toBeNull();\n    }\n\n    // Basic content checks\n    expect(jsonData, \"JSON data should not be null or undefined\").toBeTruthy();\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"WebPage\"); // Matches the testData in the example page\n    expect(jsonData.name).toBe(\"E2E Test Page for JSON-LD\");\n    expect(jsonData.description).toBe(\n      \"This page tests the JsonLdScript component.\",\n    );\n    expect(jsonData.url).toBe(\"http://localhost:3001/jsonld-test-page\");\n\n    // You can add more specific checks based on the testData\n    // For example, checking for the absence of unexpected properties\n  });\n\n  test(\"should not find the script if it has a different ID\", async ({\n    page,\n  }) => {\n    await page.goto(PAGE_URL);\n    // Try to find a script with an ID that doesn't exist\n    const nonExistentScript = page.locator(\n      'script[type=\"application/ld+json\"]#non-existent-id',\n    );\n    await expect(nonExistentScript).toHaveCount(0); // Expect no such element\n  });\n\n  // Later, you will use Ajv here for schema validation\n  test.skip(\"TODO: should validate JSON-LD against a schema using Ajv\", async ({\n    page,\n  }) => {\n    await page.goto(PAGE_URL);\n    const scriptHandle = await page.waitForSelector(SCRIPT_SELECTOR);\n    const scriptContent = await scriptHandle.innerHTML();\n    const jsonData = JSON.parse(scriptContent);\n\n    // Placeholder for Ajv validation\n    // const ajv = new Ajv();\n    // const webPageSchema = require('../../schemas/webpage.schema.json'); // You'll create this\n    // const validate = ajv.compile(webPageSchema);\n    // const valid = validate(jsonData);\n    // if (!valid) console.error(validate.errors);\n    // expect(valid, `JSON-LD should be valid according to WebPage schema. Errors: ${JSON.stringify(validate.errors)}`).toBe(true);\n    expect(jsonData).toBeTruthy(); // Keep a basic assertion for now\n  });\n});\n"
  },
  {
    "path": "tests/e2e/jsonValidation.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"JSON-LD Validation Tests\", () => {\n  test.describe(\"Valid JSON Structure\", () => {\n    test(\"ArticleJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/article\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      // This is the critical test - JSON.parse will throw if invalid\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      // Basic structure validation\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBeTruthy();\n    });\n\n    test(\"NewsArticle variant produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/news-article\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"NewsArticle\");\n    });\n\n    test(\"BlogPosting variant produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/blog-posting\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"BlogPosting\");\n    });\n\n    test(\"RecipeJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/recipe\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Recipe\");\n    });\n\n    test(\"RecipeJsonLd with advanced features produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/recipe-advanced\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Recipe\");\n      // Check nested objects are valid\n      expect(jsonData!.nutrition).toBeDefined();\n      expect(jsonData!.aggregateRating).toBeDefined();\n      expect(jsonData!.video).toBeDefined();\n    });\n\n    test(\"HowToJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/howto\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"HowTo\");\n    });\n\n    test(\"HowToJsonLd with advanced features produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/howto-advanced\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"HowTo\");\n      // Check nested objects are valid\n      expect(jsonData!.step).toBeDefined();\n      expect(jsonData!.tool).toBeDefined();\n      expect(jsonData!.supply).toBeDefined();\n      expect(jsonData!.video).toBeDefined();\n    });\n\n    test(\"OrganizationJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/organization\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Organization\");\n    });\n\n    test(\"OnlineStore variant produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/online-store\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"OnlineStore\");\n      // Check nested merchant return policy is valid\n      if (jsonData!.hasMerchantReturnPolicy) {\n        expect(jsonData!.hasMerchantReturnPolicy[\"@type\"]).toBe(\n          \"MerchantReturnPolicy\",\n        );\n      }\n      // Check nested member program is valid\n      if (jsonData!.hasMemberProgram) {\n        expect(jsonData!.hasMemberProgram[\"@type\"]).toBe(\"MemberProgram\");\n        expect(Array.isArray(jsonData!.hasMemberProgram.hasTiers)).toBe(true);\n        jsonData!.hasMemberProgram.hasTiers.forEach((tier: unknown) => {\n          expect((tier as { \"@type\": string })[\"@type\"]).toBe(\n            \"MemberProgramTier\",\n          );\n        });\n      }\n    });\n\n    test(\"OnlineStore with loyalty programs produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/online-store-loyalty\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"OnlineStore\");\n      // Check multiple member programs are valid\n      if (jsonData!.hasMemberProgram) {\n        expect(Array.isArray(jsonData!.hasMemberProgram)).toBe(true);\n        jsonData!.hasMemberProgram.forEach((program: unknown) => {\n          const prog = program as { \"@type\": string; hasTiers: unknown };\n          expect(prog[\"@type\"]).toBe(\"MemberProgram\");\n          expect(prog.hasTiers).toBeDefined();\n        });\n      }\n    });\n\n    test(\"Organization with reviews produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/organization-reviews\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Organization\");\n      // Check review array is valid\n      expect(Array.isArray(jsonData!.review)).toBe(true);\n      jsonData!.review.forEach((review: Record<string, unknown>) => {\n        expect(review[\"@type\"]).toBe(\"Review\");\n      });\n      // Check aggregateRating is valid\n      expect(jsonData!.aggregateRating[\"@type\"]).toBe(\"AggregateRating\");\n      expect(jsonData!.aggregateRating.ratingValue).toBeDefined();\n    });\n\n    test(\"Organization with complex nested data produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/organization-advanced\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      // Check arrays are valid\n      expect(Array.isArray(jsonData!.address)).toBe(true);\n      expect(Array.isArray(jsonData!.contactPoint)).toBe(true);\n    });\n\n    test(\"ReviewJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/review\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Review\");\n      expect(jsonData!.author).toBeDefined();\n      expect(jsonData!.reviewRating).toBeDefined();\n      expect(jsonData!.itemReviewed).toBeDefined();\n    });\n\n    test(\"AggregateRatingJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/aggregate-rating\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"AggregateRating\");\n      expect(jsonData!.itemReviewed).toBeDefined();\n      expect(jsonData!.ratingValue).toBeDefined();\n      expect(jsonData!.ratingCount).toBeDefined();\n    });\n\n    test(\"Review nested in Product produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/review-advanced\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Product\");\n      expect(Array.isArray(jsonData!.review)).toBe(true);\n      expect(jsonData!.aggregateRating).toBeDefined();\n    });\n  });\n\n  test.describe(\"Edge Cases and Special Characters\", () => {\n    test(\"handles deeply nested JSON structures\", async ({ page }) => {\n      await page.goto(\"/test-nested\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      // Verify deeply nested structures are accessible\n      expect(jsonData!.nutrition[\"@type\"]).toBe(\"NutritionInformation\");\n      expect(jsonData!.recipeInstructions[0].image[\"@type\"]).toBe(\n        \"ImageObject\",\n      );\n      expect(jsonData!.recipeInstructions[0].image.width).toBe(300);\n    });\n\n    test(\"handles arrays with mixed content types\", async ({ page }) => {\n      await page.goto(\"/test-arrays\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      // Verify mixed arrays are valid\n      expect(jsonData!.author).toHaveLength(3);\n      expect(jsonData!.author[0][\"@type\"]).toBe(\"Person\");\n      expect(jsonData!.author[0].name).toBe(\"John Doe\");\n      expect(jsonData!.author[1][\"@type\"]).toBe(\"Person\");\n      expect(jsonData!.author[2][\"@type\"]).toBe(\"Organization\");\n\n      expect(jsonData!.image).toHaveLength(2);\n      expect(typeof jsonData!.image[0]).toBe(\"string\");\n      expect(jsonData!.image[1][\"@type\"]).toBe(\"ImageObject\");\n    });\n\n    test(\"preserves URL query parameters correctly\", async ({ page }) => {\n      await page.goto(\"/test-url-params\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      // Verify URL parameters are preserved correctly\n      expect(jsonData!.url).toBe(\n        \"https://example.com/article?title=yes&page=1&utm_source=google&filter=new\",\n      );\n      expect(jsonData!.mainEntityOfPage).toBe(\n        \"https://example.com/main?category=tech&sort=date\",\n      );\n      expect(jsonData!.author.url).toBe(\n        \"https://example.com/authors/john?bio=full&lang=en\",\n      );\n      expect(jsonData!.publisher.url).toBe(\n        \"https://example.com?ref=article&campaign=2024\",\n      );\n\n      // Verify the parameters can be parsed\n      const articleUrl = new URL(jsonData!.url);\n      expect(articleUrl.searchParams.get(\"title\")).toBe(\"yes\");\n      expect(articleUrl.searchParams.get(\"page\")).toBe(\"1\");\n      expect(articleUrl.searchParams.get(\"utm_source\")).toBe(\"google\");\n      expect(articleUrl.searchParams.get(\"filter\")).toBe(\"new\");\n    });\n\n    test(\"LocalBusinessJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/local-business\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"LocalBusiness\");\n    });\n\n    test(\"Restaurant variant produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/restaurant\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Restaurant\");\n      // Check nested objects are valid\n      expect(jsonData!.geo).toBeDefined();\n      expect(jsonData!.aggregateRating).toBeDefined();\n      expect(Array.isArray(jsonData!.review)).toBe(true);\n    });\n\n    test(\"Store with departments produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/store-with-departments\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Store\");\n      // Check departments array is valid\n      expect(Array.isArray(jsonData!.department)).toBe(true);\n      expect(jsonData!.department).toHaveLength(3);\n      // Check each department has valid structure\n      jsonData!.department.forEach((dept: Record<string, unknown>) => {\n        expect(dept[\"@type\"]).toBeTruthy();\n        expect(dept.name).toBeTruthy();\n      });\n    });\n\n    test(\"EventJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/event\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Event\");\n      // Check nested objects are valid\n      expect(jsonData!.location).toBeDefined();\n      expect(jsonData!.location[\"@type\"]).toBe(\"Place\");\n      expect(jsonData!.offers).toBeDefined();\n      expect(jsonData!.offers[\"@type\"]).toBe(\"Offer\");\n    });\n\n    test(\"Cancelled Event produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/event-cancelled\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Event\");\n      expect(jsonData!.eventStatus).toBe(\"https://schema.org/EventCancelled\");\n    });\n\n    test(\"Rescheduled Event produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/event-rescheduled\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Event\");\n      expect(jsonData!.eventStatus).toBe(\"https://schema.org/EventRescheduled\");\n      // Check arrays are valid\n      expect(Array.isArray(jsonData!.previousStartDate)).toBe(true);\n      expect(Array.isArray(jsonData!.offers)).toBe(true);\n      expect(Array.isArray(jsonData!.performer)).toBe(true);\n    });\n\n    test(\"Free Event produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/event-free\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Event\");\n      // Check free event has price 0\n      expect(jsonData!.offers.price).toBe(0);\n      expect(jsonData!.offers[\"@type\"]).toBe(\"Offer\");\n    });\n\n    test(\"FAQJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/faq\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"FAQPage\");\n      // Check mainEntity array is valid\n      expect(Array.isArray(jsonData!.mainEntity)).toBe(true);\n      expect(jsonData!.mainEntity.length).toBeGreaterThan(0);\n      // Check each question has valid structure\n      jsonData!.mainEntity.forEach((question: Record<string, unknown>) => {\n        expect(question[\"@type\"]).toBe(\"Question\");\n        expect(question.name).toBeTruthy();\n        expect(question.acceptedAnswer).toBeTruthy();\n        const answer = question.acceptedAnswer as Record<string, unknown>;\n        expect(answer[\"@type\"]).toBe(\"Answer\");\n        expect(answer.text).toBeTruthy();\n      });\n    });\n\n    test(\"Advanced FAQJsonLd with HTML produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/faq-advanced\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"FAQPage\");\n      expect(Array.isArray(jsonData!.mainEntity)).toBe(true);\n\n      // Check HTML content is preserved properly\n      const firstAnswer = jsonData!.mainEntity[0].acceptedAnswer.text;\n      expect(firstAnswer).toContain(\"<p>\");\n      expect(firstAnswer).toContain(\"<ul>\");\n      expect(firstAnswer).toContain(\"<li>\");\n      expect(firstAnswer).toContain(\"<a href=\");\n    });\n\n    test(\"Health-focused FAQJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/faq-health\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"FAQPage\");\n      expect(Array.isArray(jsonData!.mainEntity)).toBe(true);\n      expect(jsonData!.mainEntity).toHaveLength(5);\n\n      // Verify complex HTML structures are valid\n      jsonData!.mainEntity.forEach((question: Record<string, unknown>) => {\n        expect(question[\"@type\"]).toBe(\"Question\");\n        const answer = question.acceptedAnswer as Record<string, unknown>;\n        expect(answer[\"@type\"]).toBe(\"Answer\");\n        expect(typeof answer.text).toBe(\"string\");\n      });\n    });\n\n    test(\"QuizJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/quiz\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org/\");\n      expect(jsonData![\"@type\"]).toBe(\"Quiz\");\n      // Check hasPart array is valid\n      expect(Array.isArray(jsonData!.hasPart)).toBe(true);\n      expect(jsonData!.hasPart.length).toBeGreaterThan(0);\n      // Check each question has valid structure\n      jsonData!.hasPart.forEach((question: Record<string, unknown>) => {\n        expect(question[\"@type\"]).toBe(\"Question\");\n        expect(question.eduQuestionType).toBe(\"Flashcard\");\n        expect(question.text).toBeTruthy();\n        expect(question.acceptedAnswer).toBeTruthy();\n        const answer = question.acceptedAnswer as Record<string, unknown>;\n        expect(answer[\"@type\"]).toBe(\"Answer\");\n        expect(answer.text).toBeTruthy();\n      });\n    });\n\n    test(\"Biology QuizJsonLd with educational alignment produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/quiz-biology\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org/\");\n      expect(jsonData![\"@type\"]).toBe(\"Quiz\");\n      // Check about property\n      expect(jsonData!.about).toBeDefined();\n      expect(jsonData!.about[\"@type\"]).toBe(\"Thing\");\n      expect(jsonData!.about.name).toBe(\"Cell Biology\");\n      // Check educational alignment\n      expect(Array.isArray(jsonData!.educationalAlignment)).toBe(true);\n      expect(jsonData!.educationalAlignment).toHaveLength(2);\n      jsonData!.educationalAlignment.forEach(\n        (alignment: Record<string, unknown>) => {\n          expect(alignment[\"@type\"]).toBe(\"AlignmentObject\");\n          expect(alignment.alignmentType).toBeTruthy();\n          expect(alignment.targetName).toBeTruthy();\n        },\n      );\n    });\n\n    test(\"Advanced QuizJsonLd with all features produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/quiz-advanced\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org/\");\n      expect(jsonData![\"@type\"]).toBe(\"Quiz\");\n      // Check about property with full Thing object\n      expect(jsonData!.about).toBeDefined();\n      expect(jsonData!.about[\"@type\"]).toBe(\"Thing\");\n      expect(jsonData!.about.name).toBeTruthy();\n      expect(jsonData!.about.description).toBeTruthy();\n      expect(jsonData!.about.url).toBeTruthy();\n      // Check mixed question formats are all valid\n      expect(Array.isArray(jsonData!.hasPart)).toBe(true);\n      expect(jsonData!.hasPart).toHaveLength(4);\n      // Verify all questions have required properties\n      jsonData!.hasPart.forEach((question: Record<string, unknown>) => {\n        expect(question[\"@type\"]).toBe(\"Question\");\n        expect(question.eduQuestionType).toBe(\"Flashcard\");\n        expect(typeof question.text).toBe(\"string\");\n        expect(question.acceptedAnswer).toBeTruthy();\n      });\n    });\n\n    test(\"MovieCarouselJsonLd summary page produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/movie-carousel-summary\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ItemList\");\n      // Check itemListElement array is valid\n      expect(Array.isArray(jsonData!.itemListElement)).toBe(true);\n      expect(jsonData!.itemListElement).toHaveLength(5);\n      // Check each list item has valid structure\n      jsonData!.itemListElement.forEach((item: Record<string, unknown>) => {\n        expect(item[\"@type\"]).toBe(\"ListItem\");\n        expect(item.position).toBeTruthy();\n        expect(item.url).toBeTruthy();\n        expect(typeof item.url).toBe(\"string\");\n      });\n    });\n\n    test(\"MovieCarouselJsonLd all-in-one page produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/movie-carousel\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ItemList\");\n      // Check itemListElement array is valid\n      expect(Array.isArray(jsonData!.itemListElement)).toBe(true);\n      expect(jsonData!.itemListElement).toHaveLength(3);\n      // Check each list item has valid movie structure\n      jsonData!.itemListElement.forEach((item: Record<string, unknown>) => {\n        expect(item[\"@type\"]).toBe(\"ListItem\");\n        expect(item.position).toBeTruthy();\n        expect(item.item).toBeTruthy();\n        const movie = item.item as Record<string, unknown>;\n        expect(movie[\"@type\"]).toBe(\"Movie\");\n        expect(movie.name).toBeTruthy();\n        expect(movie.image).toBeTruthy();\n        // Check nested objects are valid\n        if (movie.director) {\n          const director = movie.director as Record<string, unknown>;\n          expect(director[\"@type\"]).toBe(\"Person\");\n        }\n        if (movie.review) {\n          const review = movie.review as Record<string, unknown>;\n          expect(review[\"@type\"]).toBe(\"Review\");\n        }\n        if (movie.aggregateRating) {\n          const rating = movie.aggregateRating as Record<string, unknown>;\n          expect(rating[\"@type\"]).toBe(\"AggregateRating\");\n        }\n      });\n    });\n\n    test(\"MovieCarouselJsonLd with advanced features produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/movie-carousel-advanced\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ItemList\");\n      expect(Array.isArray(jsonData!.itemListElement)).toBe(true);\n      expect(jsonData!.itemListElement).toHaveLength(3);\n\n      // Check complex nested structures\n      const firstMovie = jsonData!.itemListElement[0].item;\n      // Check image array with mixed types\n      expect(Array.isArray(firstMovie.image)).toBe(true);\n      expect(firstMovie.image).toHaveLength(4);\n      expect(typeof firstMovie.image[0]).toBe(\"string\");\n      expect(firstMovie.image[1][\"@type\"]).toBe(\"ImageObject\");\n      expect(firstMovie.image[1].width).toBe(1200);\n      expect(firstMovie.image[1].height).toBe(900);\n      // Check director with URL\n      expect(firstMovie.director[\"@type\"]).toBe(\"Person\");\n      expect(firstMovie.director.url).toBeTruthy();\n      // Check complete review structure\n      expect(firstMovie.review[\"@type\"]).toBe(\"Review\");\n      expect(firstMovie.review.reviewRating[\"@type\"]).toBe(\"Rating\");\n      expect(firstMovie.review.reviewRating.ratingValue).toBeTruthy();\n      expect(firstMovie.review.author[\"@type\"]).toBe(\"Person\");\n      expect(firstMovie.review.reviewBody).toBeTruthy();\n      // Check aggregate rating with all fields\n      expect(firstMovie.aggregateRating[\"@type\"]).toBe(\"AggregateRating\");\n      expect(firstMovie.aggregateRating.ratingValue).toBeTruthy();\n      expect(firstMovie.aggregateRating.reviewCount).toBeTruthy();\n    });\n\n    test(\"DatasetJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/dataset\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Dataset\");\n      expect(jsonData!.name).toBeTruthy();\n      expect(jsonData!.description).toBeTruthy();\n    });\n\n    test(\"DatasetJsonLd with advanced features produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/dataset-advanced\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Dataset\");\n\n      // Verify complex nested structures parse correctly\n      expect(Array.isArray(jsonData!.creator)).toBe(true);\n      expect(jsonData!.creator[0][\"@type\"]).toBe(\"Organization\");\n      expect(jsonData!.creator[0].contactPoint[\"@type\"]).toBe(\"ContactPoint\");\n\n      expect(Array.isArray(jsonData!.distribution)).toBe(true);\n      expect(jsonData!.distribution[0][\"@type\"]).toBe(\"DataDownload\");\n\n      expect(jsonData!.spatialCoverage[\"@type\"]).toBe(\"Place\");\n      expect(jsonData!.spatialCoverage.geo[\"@type\"]).toBe(\"GeoShape\");\n\n      expect(Array.isArray(jsonData!.variableMeasured)).toBe(true);\n      expect(jsonData!.variableMeasured[2][\"@type\"]).toBe(\"PropertyValue\");\n    });\n\n    test(\"DatasetJsonLd with catalog produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/dataset-catalog\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Dataset\");\n      expect(jsonData!.includedInDataCatalog[\"@type\"]).toBe(\"DataCatalog\");\n    });\n\n    test(\"DatasetJsonLd with nested datasets produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/dataset-nested\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Dataset\");\n\n      // Verify hasPart array with nested datasets\n      expect(Array.isArray(jsonData!.hasPart)).toBe(true);\n      expect(jsonData!.hasPart).toHaveLength(3);\n      expect(jsonData!.hasPart[0][\"@type\"]).toBe(\"Dataset\");\n      expect(jsonData!.hasPart[0].name).toBeTruthy();\n      expect(jsonData!.hasPart[0].distribution[\"@type\"]).toBe(\"DataDownload\");\n    });\n\n    test(\"JobPostingJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/job-posting\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"JobPosting\");\n      expect(jsonData!.title).toBeTruthy();\n      expect(jsonData!.description).toBeTruthy();\n      expect(jsonData!.datePosted).toBeTruthy();\n      expect(jsonData!.hiringOrganization).toBeDefined();\n      expect(jsonData!.hiringOrganization[\"@type\"]).toBe(\"Organization\");\n    });\n\n    test(\"Remote JobPostingJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/job-posting-remote\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"JobPosting\");\n      expect(jsonData!.jobLocationType).toBe(\"TELECOMMUTE\");\n      expect(jsonData!.applicantLocationRequirements).toBeDefined();\n      expect(jsonData!.baseSalary).toBeDefined();\n      expect(jsonData!.baseSalary[\"@type\"]).toBe(\"MonetaryAmount\");\n      expect(jsonData!.baseSalary.value[\"@type\"]).toBe(\"QuantitativeValue\");\n    });\n\n    test(\"Advanced JobPostingJsonLd with all features produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/job-posting-advanced\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"JobPosting\");\n\n      // Verify multiple job locations\n      expect(Array.isArray(jsonData!.jobLocation)).toBe(true);\n      expect(jsonData!.jobLocation).toHaveLength(2);\n      expect(jsonData!.jobLocation[0][\"@type\"]).toBe(\"Place\");\n      expect(jsonData!.jobLocation[0].address[\"@type\"]).toBe(\"PostalAddress\");\n\n      // Verify multiple applicant location requirements\n      expect(Array.isArray(jsonData!.applicantLocationRequirements)).toBe(true);\n      expect(\n        jsonData!.applicantLocationRequirements.every(\n          (loc: Record<string, unknown>) =>\n            loc[\"@type\"] === \"State\" || loc[\"@type\"] === \"Country\",\n        ),\n      ).toBe(true);\n\n      // Verify multiple employment types\n      expect(Array.isArray(jsonData!.employmentType)).toBe(true);\n\n      // Verify education requirements array\n      expect(Array.isArray(jsonData!.educationRequirements)).toBe(true);\n      expect(jsonData!.educationRequirements[0][\"@type\"]).toBe(\n        \"EducationalOccupationalCredential\",\n      );\n\n      // Verify experience requirements\n      expect(jsonData!.experienceRequirements[\"@type\"]).toBe(\n        \"OccupationalExperienceRequirements\",\n      );\n\n      // Verify identifier\n      expect(jsonData!.identifier[\"@type\"]).toBe(\"PropertyValue\");\n\n      // Verify organization with logo\n      expect(jsonData!.hiringOrganization.logo[\"@type\"]).toBe(\"ImageObject\");\n    });\n\n    test(\"DiscussionForumPostingJsonLd produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/discussion-forum-advanced\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"DiscussionForumPosting\");\n\n      // Verify nested comment structure\n      expect(Array.isArray(jsonData!.comment)).toBe(true);\n      expect(jsonData!.comment[0][\"@type\"]).toBe(\"Comment\");\n      expect(jsonData!.comment[0].author[\"@type\"]).toBe(\"Person\");\n\n      // Verify nested comments within comments\n      expect(Array.isArray(jsonData!.comment[0].comment)).toBe(true);\n      expect(jsonData!.comment[0].comment[0][\"@type\"]).toBe(\"Comment\");\n\n      // Verify interaction statistics\n      expect(Array.isArray(jsonData!.interactionStatistic)).toBe(true);\n      expect(jsonData!.interactionStatistic[0][\"@type\"]).toBe(\n        \"InteractionCounter\",\n      );\n\n      // Verify video object\n      expect(jsonData!.video[\"@type\"]).toBe(\"VideoObject\");\n\n      // Verify isPartOf\n      expect(jsonData!.isPartOf[\"@type\"]).toBe(\"CreativeWork\");\n\n      // Verify sharedContent\n      expect(jsonData!.sharedContent[\"@type\"]).toBe(\"WebPage\");\n    });\n\n    test(\"VacationRentalJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/vacation-rental-advanced\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"VacationRental\");\n\n      // Verify required properties\n      expect(jsonData!.containsPlace).toBeDefined();\n      expect(jsonData!.containsPlace[\"@type\"]).toBe(\"Accommodation\");\n      expect(jsonData!.containsPlace.occupancy[\"@type\"]).toBe(\n        \"QuantitativeValue\",\n      );\n      expect(jsonData!.identifier).toBeTruthy();\n      expect(Array.isArray(jsonData!.image)).toBe(true);\n      expect(jsonData!.image.length).toBeGreaterThanOrEqual(8);\n      expect(jsonData!.latitude).toBeDefined();\n      expect(jsonData!.longitude).toBeDefined();\n      expect(jsonData!.name).toBeTruthy();\n\n      // Verify nested structures\n      expect(jsonData!.address[\"@type\"]).toBe(\"PostalAddress\");\n      expect(jsonData!.aggregateRating[\"@type\"]).toBe(\"AggregateRating\");\n      expect(jsonData!.brand[\"@type\"]).toBe(\"Brand\");\n\n      // Verify bed details\n      expect(Array.isArray(jsonData!.containsPlace.bed)).toBe(true);\n      expect(jsonData!.containsPlace.bed[0][\"@type\"]).toBe(\"BedDetails\");\n\n      // Verify amenity features\n      expect(Array.isArray(jsonData!.containsPlace.amenityFeature)).toBe(true);\n      expect(jsonData!.containsPlace.amenityFeature[0][\"@type\"]).toBe(\n        \"LocationFeatureSpecification\",\n      );\n\n      // Verify reviews\n      expect(Array.isArray(jsonData!.review)).toBe(true);\n      expect(jsonData!.review[0][\"@type\"]).toBe(\"Review\");\n      expect(jsonData!.review[0].author[\"@type\"]).toBe(\"Person\");\n    });\n\n    test(\"CourseJsonLd single course produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/course\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Course\");\n\n      // Verify required properties\n      expect(jsonData!.name).toBeTruthy();\n      expect(jsonData!.description).toBeTruthy();\n\n      // Verify provider is Organization\n      expect(jsonData!.provider).toBeDefined();\n      expect(jsonData!.provider[\"@type\"]).toBe(\"Organization\");\n    });\n\n    test(\"CourseJsonLd list produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/course-list\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ItemList\");\n\n      // Verify ItemList structure\n      expect(Array.isArray(jsonData!.itemListElement)).toBe(true);\n      expect(jsonData!.itemListElement.length).toBeGreaterThanOrEqual(3);\n\n      // Verify each item\n      jsonData!.itemListElement.forEach(\n        (item: {\n          \"@type\": string;\n          position: number;\n          item: { \"@type\": string; name: string; description: string };\n        }) => {\n          expect(item[\"@type\"]).toBe(\"ListItem\");\n          expect(item.position).toBeGreaterThan(0);\n          expect(item.item).toBeDefined();\n          expect(item.item[\"@type\"]).toBe(\"Course\");\n          expect(item.item.name).toBeTruthy();\n          expect(item.item.description).toBeTruthy();\n        },\n      );\n    });\n\n    test(\"CourseJsonLd summary list produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/course-list-summary\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ItemList\");\n\n      // Verify ItemList structure for summary page\n      expect(Array.isArray(jsonData!.itemListElement)).toBe(true);\n      expect(jsonData!.itemListElement.length).toBeGreaterThanOrEqual(3);\n\n      // Verify each item has only URL, not full course data\n      jsonData!.itemListElement.forEach(\n        (item: {\n          \"@type\": string;\n          position: number;\n          url: string;\n          item?: unknown;\n        }) => {\n          expect(item[\"@type\"]).toBe(\"ListItem\");\n          expect(item.position).toBeGreaterThan(0);\n          expect(item.url).toBeTruthy();\n          expect(item.item).toBeUndefined(); // Should not have item for summary\n        },\n      );\n    });\n\n    test(\"ProfilePageJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/profile\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ProfilePage\");\n\n      // Verify required mainEntity property\n      expect(jsonData!.mainEntity).toBeDefined();\n      expect(jsonData!.mainEntity[\"@type\"]).toBeDefined();\n    });\n\n    test(\"ProfilePageJsonLd with Person produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/profile-advanced\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ProfilePage\");\n      expect(jsonData!.mainEntity[\"@type\"]).toBe(\"Person\");\n\n      // Verify interaction statistics have proper @type\n      expect(jsonData!.mainEntity.interactionStatistic).toBeDefined();\n      jsonData!.mainEntity.interactionStatistic.forEach(\n        (stat: { \"@type\": string }) => {\n          expect(stat[\"@type\"]).toBe(\"InteractionCounter\");\n        },\n      );\n    });\n\n    test(\"ProfilePageJsonLd with Organization produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/profile-organization\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ProfilePage\");\n      expect(jsonData!.mainEntity[\"@type\"]).toBe(\"Organization\");\n    });\n\n    test(\"SoftwareApplicationJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/software-app\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"SoftwareApplication\");\n      // Verify required properties\n      expect(jsonData!.name).toBeDefined();\n      expect(jsonData!.offers).toBeDefined();\n      expect(jsonData!.aggregateRating).toBeDefined();\n    });\n\n    test(\"MobileApplication variant produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/mobile-app\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"MobileApplication\");\n    });\n\n    test(\"VideoGame co-typed produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/video-game\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toEqual([\"VideoGame\", \"MobileApplication\"]);\n    });\n\n    test(\"EmployerAggregateRatingJsonLd produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/employer-aggregate-rating\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"EmployerAggregateRating\");\n      // Verify required properties\n      expect(jsonData!.itemReviewed).toBeDefined();\n      expect(jsonData!.itemReviewed[\"@type\"]).toBe(\"Organization\");\n      expect(jsonData!.ratingValue).toBeDefined();\n      expect(jsonData!.ratingCount || jsonData!.reviewCount).toBeTruthy();\n    });\n\n    test(\"ClaimReviewJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/claim-review\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ClaimReview\");\n      // Verify required properties\n      expect(jsonData!.claimReviewed).toBeTruthy();\n      expect(jsonData!.reviewRating).toBeDefined();\n      expect(jsonData!.reviewRating[\"@type\"]).toBe(\"Rating\");\n      expect(jsonData!.reviewRating.alternateName).toBeTruthy();\n      expect(jsonData!.url).toBeTruthy();\n    });\n\n    test(\"ImageJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/image\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ImageObject\");\n      // Verify required properties\n      expect(jsonData!.contentUrl).toBeTruthy();\n      expect(jsonData!.creator).toBeDefined();\n      expect(jsonData!.creator[\"@type\"]).toBe(\"Person\");\n    });\n\n    test(\"ImageJsonLd with multiple images produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/image-multiple\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@graph\"]).toBeDefined();\n      expect(Array.isArray(jsonData![\"@graph\"])).toBe(true);\n      expect(jsonData![\"@graph\"]).toHaveLength(3);\n      // Verify each image has required properties\n      jsonData![\"@graph\"].forEach((image: Record<string, unknown>) => {\n        expect(image[\"@type\"]).toBe(\"ImageObject\");\n        expect(image.contentUrl).toBeTruthy();\n      });\n    });\n\n    test(\"VideoJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/video\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"VideoObject\");\n      // Verify required properties\n      expect(jsonData!.name).toBeTruthy();\n      expect(jsonData!.description).toBeTruthy();\n      expect(jsonData!.thumbnailUrl).toBeTruthy();\n      expect(jsonData!.uploadDate).toBeTruthy();\n    });\n\n    test(\"VideoJsonLd with BroadcastEvent produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/video-live\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"VideoObject\");\n      expect(jsonData!.publication).toBeDefined();\n      expect(Array.isArray(jsonData!.publication)).toBe(true);\n      expect(jsonData!.publication[0][\"@type\"]).toBe(\"BroadcastEvent\");\n    });\n\n    test(\"VideoJsonLd with Clips produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/video-clips\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"VideoObject\");\n      expect(jsonData!.hasPart).toBeDefined();\n      expect(Array.isArray(jsonData!.hasPart)).toBe(true);\n      expect(jsonData!.hasPart[0][\"@type\"]).toBe(\"Clip\");\n    });\n\n    test(\"CarouselJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/carousel-summary\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ItemList\");\n      expect(jsonData!.itemListElement).toBeDefined();\n      expect(Array.isArray(jsonData!.itemListElement)).toBe(true);\n    });\n\n    test(\"CarouselJsonLd with Courses produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/carousel-course\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ItemList\");\n      expect(jsonData!.itemListElement[0].item[\"@type\"]).toBe(\"Course\");\n    });\n\n    test(\"CarouselJsonLd with Recipes produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/carousel-recipe\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ItemList\");\n      expect(jsonData!.itemListElement[0].item[\"@type\"]).toBe(\"Recipe\");\n    });\n\n    test(\"CarouselJsonLd with Restaurants produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/carousel-restaurant\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"ItemList\");\n      expect(jsonData!.itemListElement[0].item[\"@type\"]).toBe(\"Restaurant\");\n    });\n\n    test(\"CreativeWorkJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/creative-work\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Article\");\n      // Verify hasPart for paywalled content\n      expect(jsonData!.hasPart).toBeDefined();\n      expect(jsonData!.hasPart[\"@type\"]).toBe(\"WebPageElement\");\n    });\n\n    test(\"CreativeWorkJsonLd with multiple paywalled sections produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/creative-work-multiple\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Article\");\n      // Verify multiple hasPart sections\n      expect(Array.isArray(jsonData!.hasPart)).toBe(true);\n      expect(jsonData!.hasPart).toHaveLength(2);\n      jsonData!.hasPart.forEach((part: Record<string, unknown>) => {\n        expect(part[\"@type\"]).toBe(\"WebPageElement\");\n        expect(part.isAccessibleForFree).toBe(false);\n        expect(part.cssSelector).toBeTruthy();\n      });\n    });\n\n    test(\"CreativeWorkJsonLd NewsArticle variant produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/creative-work-news\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"NewsArticle\");\n      expect(jsonData!.isAccessibleForFree).toBe(false);\n      expect(jsonData!.hasPart[\"@type\"]).toBe(\"WebPageElement\");\n    });\n\n    test(\"CreativeWorkJsonLd Blog variant produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/creative-work-blog\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Blog\");\n      expect(jsonData!.isAccessibleForFree).toBe(false);\n      expect(jsonData!.hasPart[\"@type\"]).toBe(\"WebPageElement\");\n    });\n\n    test(\"ProductJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/product\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Product\");\n    });\n\n    test(\"ProductJsonLd with review and pros/cons produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/product-review\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Product\");\n      expect(jsonData!.review).toBeDefined();\n      expect(jsonData!.review.positiveNotes).toBeDefined();\n      expect(jsonData!.review.negativeNotes).toBeDefined();\n    });\n\n    test(\"ProductJsonLd with AggregateOffer produces valid JSON\", async ({\n      page,\n    }) => {\n      await page.goto(\"/product-aggregate\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Product\");\n      expect(jsonData!.offers).toBeDefined();\n      expect(jsonData!.offers[\"@type\"]).toBe(\"AggregateOffer\");\n    });\n\n    test(\"Custom PodcastSeriesJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/custom-podcast\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"PodcastSeries\");\n      expect(jsonData!.name).toBeTruthy();\n      expect(jsonData!.host).toBeDefined();\n      expect(jsonData!.host[\"@type\"]).toBe(\"Person\");\n      expect(Array.isArray(jsonData!.episode)).toBe(true);\n    });\n\n    test(\"Custom ServiceJsonLd produces valid JSON\", async ({ page }) => {\n      await page.goto(\"/custom-service\");\n\n      const jsonLdScript = await page\n        .locator('script[type=\"application/ld+json\"]')\n        .textContent();\n\n      expect(jsonLdScript).toBeTruthy();\n\n      let jsonData;\n      expect(() => {\n        jsonData = JSON.parse(jsonLdScript!);\n      }).not.toThrow();\n\n      expect(jsonData).toBeDefined();\n      expect(jsonData![\"@context\"]).toBe(\"https://schema.org\");\n      expect(jsonData![\"@type\"]).toBe(\"Service\");\n      expect(jsonData!.name).toBeTruthy();\n      expect(jsonData!.provider).toBeDefined();\n      expect(jsonData!.provider[\"@type\"]).toBe(\"Organization\");\n      expect(Array.isArray(jsonData!.areaServed)).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e/localBusinessJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"LocalBusinessJsonLd\", () => {\n  test(\"renders basic LocalBusiness structured data\", async ({ page }) => {\n    await page.goto(\"/local-business\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify all properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"LocalBusiness\");\n    expect(jsonData.name).toBe(\"Gary's Tech Repair Shop\");\n    expect(jsonData.address).toEqual({\n      \"@type\": \"PostalAddress\",\n      streetAddress: \"123 Tech Street\",\n      addressLocality: \"San Francisco\",\n      addressRegion: \"CA\",\n      postalCode: \"94102\",\n      addressCountry: \"US\",\n    });\n    expect(jsonData.telephone).toBe(\"+14155551234\");\n    expect(jsonData.url).toBe(\"https://example.com/locations/sf\");\n    expect(jsonData.description).toBe(\n      \"Professional computer and phone repair services in San Francisco\",\n    );\n    expect(jsonData.openingHoursSpecification).toHaveLength(2);\n  });\n\n  test(\"renders Restaurant with all features\", async ({ page }) => {\n    await page.goto(\"/restaurant\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify restaurant-specific properties\n    expect(jsonData[\"@type\"]).toBe(\"Restaurant\");\n    expect(jsonData.name).toBe(\"The Golden Fork\");\n    expect(jsonData.geo).toEqual({\n      \"@type\": \"GeoCoordinates\",\n      latitude: 40.7489,\n      longitude: -73.968,\n    });\n    expect(jsonData.image).toHaveLength(3);\n    expect(jsonData.servesCuisine).toEqual([\n      \"Italian\",\n      \"Mediterranean\",\n      \"Vegetarian\",\n    ]);\n    expect(jsonData.priceRange).toBe(\"$$$\");\n    expect(jsonData.menu).toBe(\n      \"https://example.com/restaurants/golden-fork/menu\",\n    );\n\n    // Verify aggregate rating\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.6,\n      ratingCount: 892,\n      reviewCount: 846,\n      bestRating: 5,\n      worstRating: 1,\n    });\n\n    // Verify reviews\n    expect(jsonData.review).toHaveLength(2);\n    expect(jsonData.review[0].author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Sarah Johnson\",\n    });\n    expect(jsonData.review[0].reviewRating.ratingValue).toBe(5);\n\n    // Verify opening hours\n    expect(jsonData.openingHoursSpecification).toHaveLength(3);\n    expect(jsonData.openingHoursSpecification[0].dayOfWeek).toEqual([\n      \"Monday\",\n      \"Tuesday\",\n      \"Wednesday\",\n      \"Thursday\",\n    ]);\n\n    // Verify boolean values\n    expect(jsonData.publicAccess).toBe(true);\n    expect(jsonData.smokingAllowed).toBe(false);\n    expect(jsonData.isAccessibleForFree).toBe(true);\n  });\n\n  test(\"renders Store with departments\", async ({ page }) => {\n    await page.goto(\"/store-with-departments\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify store type\n    expect(jsonData[\"@type\"]).toBe(\"Store\");\n    expect(jsonData.name).toBe(\"MegaMart Superstore\");\n\n    // Verify departments\n    expect(jsonData.department).toHaveLength(3);\n\n    // Check pharmacy department\n    const pharmacy = jsonData.department[0];\n    expect(pharmacy[\"@type\"]).toBe(\"Pharmacy\");\n    expect(pharmacy.name).toBe(\"MegaMart Pharmacy\");\n    expect(pharmacy.telephone).toBe(\"+13235554322\");\n    expect(pharmacy.openingHoursSpecification).toHaveLength(3);\n\n    // Check auto parts department\n    const autoParts = jsonData.department[1];\n    expect(autoParts[\"@type\"]).toBe(\"AutoPartsStore\");\n    expect(autoParts.name).toBe(\"MegaMart Auto Center\");\n\n    // Check bakery department\n    const bakery = jsonData.department[2];\n    expect(bakery[\"@type\"]).toBe(\"Bakery\");\n    expect(bakery.name).toBe(\"MegaMart Fresh Bakery\");\n\n    // Verify area served\n    expect(jsonData.areaServed).toEqual([\n      \"Los Angeles\",\n      \"Hollywood\",\n      \"West Hollywood\",\n      \"Beverly Hills\",\n    ]);\n  });\n});\n"
  },
  {
    "path": "tests/e2e/merchantReturnPolicyJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"MerchantReturnPolicyJsonLd\", () => {\n  test(\"renders basic return policy structured data\", async ({ page }) => {\n    await page.goto(\"/merchant-return-policy\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"MerchantReturnPolicy\");\n\n    // Verify required properties\n    expect(jsonData.applicableCountry).toEqual([\"US\", \"CA\"]);\n    expect(jsonData.returnPolicyCategory).toBe(\n      \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n    );\n    expect(jsonData.merchantReturnDays).toBe(30);\n\n    // Verify additional properties\n    expect(jsonData.returnPolicyCountry).toEqual([\"US\"]);\n    expect(jsonData.returnMethod).toEqual([\"https://schema.org/ReturnByMail\"]);\n    expect(jsonData.returnFees).toBe(\"https://schema.org/FreeReturn\");\n    expect(jsonData.refundType).toEqual([\"https://schema.org/FullRefund\"]);\n    expect(jsonData.returnLabelSource).toBe(\n      \"https://schema.org/ReturnLabelDownloadAndPrint\",\n    );\n  });\n\n  test(\"renders advanced return policy with all features\", async ({ page }) => {\n    await page.goto(\"/merchant-return-policy-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic structure\n    expect(jsonData[\"@type\"]).toBe(\"MerchantReturnPolicy\");\n    expect(jsonData.applicableCountry).toEqual([\"DE\", \"AT\", \"CH\"]);\n    expect(jsonData.returnPolicyCountry).toEqual([\"IE\"]);\n\n    // Verify arrays\n    expect(jsonData.itemCondition).toEqual([\n      \"https://schema.org/NewCondition\",\n      \"https://schema.org/DamagedCondition\",\n    ]);\n    expect(jsonData.returnMethod).toEqual([\n      \"https://schema.org/ReturnByMail\",\n      \"https://schema.org/ReturnInStore\",\n    ]);\n    expect(jsonData.refundType).toEqual([\n      \"https://schema.org/FullRefund\",\n      \"https://schema.org/ExchangeRefund\",\n    ]);\n\n    // Verify MonetaryAmount fields\n    expect(jsonData.returnShippingFeesAmount).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      value: 2.99,\n      currency: \"EUR\",\n    });\n    expect(jsonData.customerRemorseReturnShippingFeesAmount).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      value: 5.99,\n      currency: \"EUR\",\n    });\n    expect(jsonData.restockingFee).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      value: 10,\n      currency: \"EUR\",\n    });\n\n    // Verify customer remorse properties\n    expect(jsonData.customerRemorseReturnFees).toBe(\n      \"https://schema.org/ReturnShippingFees\",\n    );\n    expect(jsonData.customerRemorseReturnLabelSource).toBe(\n      \"https://schema.org/ReturnLabelDownloadAndPrint\",\n    );\n\n    // Verify item defect properties\n    expect(jsonData.itemDefectReturnFees).toBe(\"https://schema.org/FreeReturn\");\n    expect(jsonData.itemDefectReturnLabelSource).toBe(\n      \"https://schema.org/ReturnLabelInBox\",\n    );\n\n    // Verify seasonal override\n    expect(jsonData.returnPolicySeasonalOverride).toEqual({\n      \"@type\": \"MerchantReturnPolicySeasonalOverride\",\n      startDate: \"2025-12-01\",\n      endDate: \"2025-01-05\",\n      returnPolicyCategory:\n        \"https://schema.org/MerchantReturnFiniteReturnWindow\",\n      merchantReturnDays: 30,\n    });\n  });\n\n  test(\"renders return policy with merchantReturnLink only\", async ({\n    page,\n  }) => {\n    await page.goto(\"/merchant-return-policy-link\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"MerchantReturnPolicy\");\n    expect(jsonData.merchantReturnLink).toBe(\"https://www.example.com/returns\");\n\n    // Verify no other properties are present when using merchantReturnLink\n    expect(jsonData.applicableCountry).toBeUndefined();\n    expect(jsonData.returnPolicyCategory).toBeUndefined();\n    expect(jsonData.merchantReturnDays).toBeUndefined();\n  });\n\n  test(\"renders product with return policy in offer\", async ({ page }) => {\n    await page.goto(\"/product-with-return-policy\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify product structure\n    expect(jsonData[\"@type\"]).toBe(\"Product\");\n    expect(jsonData.name).toBe(\"Premium Wireless Headphones\");\n\n    // Verify offer has return policy\n    expect(jsonData.offers).toBeDefined();\n    expect(jsonData.offers.hasMerchantReturnPolicy).toBeDefined();\n\n    const returnPolicy = jsonData.offers.hasMerchantReturnPolicy;\n    expect(returnPolicy[\"@type\"]).toBe(\"MerchantReturnPolicy\");\n    expect(returnPolicy.applicableCountry).toEqual([\"US\"]);\n    expect(returnPolicy.merchantReturnDays).toBe(45);\n    expect(returnPolicy.returnFees).toBe(\"https://schema.org/FreeReturn\");\n    expect(returnPolicy.itemCondition).toEqual([\n      \"https://schema.org/NewCondition\",\n      \"https://schema.org/DamagedCondition\",\n    ]);\n    expect(returnPolicy.returnMethod).toEqual([\n      \"https://schema.org/ReturnByMail\",\n      \"https://schema.org/ReturnInStore\",\n    ]);\n    expect(returnPolicy.refundType).toEqual([\n      \"https://schema.org/FullRefund\",\n      \"https://schema.org/ExchangeRefund\",\n    ]);\n  });\n\n  test(\"renders OnlineStore with enhanced return policy\", async ({ page }) => {\n    await page.goto(\"/online-store\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify OnlineStore structure\n    expect(jsonData[\"@type\"]).toBe(\"OnlineStore\");\n    expect(jsonData.name).toBe(\"Example Online Store\");\n\n    // Verify hasMerchantReturnPolicy\n    expect(jsonData.hasMerchantReturnPolicy).toBeDefined();\n\n    const returnPolicy = jsonData.hasMerchantReturnPolicy;\n    expect(returnPolicy[\"@type\"]).toBe(\"MerchantReturnPolicy\");\n    expect(returnPolicy.applicableCountry).toEqual([\"US\", \"CA\"]);\n    expect(returnPolicy.returnPolicyCountry).toEqual([\"US\"]);\n    expect(returnPolicy.merchantReturnDays).toBe(60);\n\n    // Verify seasonal overrides array\n    expect(returnPolicy.returnPolicySeasonalOverride).toBeDefined();\n    expect(Array.isArray(returnPolicy.returnPolicySeasonalOverride)).toBe(true);\n    expect(returnPolicy.returnPolicySeasonalOverride).toHaveLength(1);\n\n    const seasonalOverride = returnPolicy.returnPolicySeasonalOverride[0];\n    expect(seasonalOverride[\"@type\"]).toBe(\n      \"MerchantReturnPolicySeasonalOverride\",\n    );\n    expect(seasonalOverride.startDate).toBe(\"2025-11-29\");\n    expect(seasonalOverride.endDate).toBe(\"2025-12-31\");\n    expect(seasonalOverride.merchantReturnDays).toBe(90);\n  });\n});\n\ntest.describe(\"MerchantReturnPolicyJsonLd escaping\", () => {\n  test(\"properly escapes special characters in return policy\", async ({\n    page,\n  }) => {\n    // This would require creating a test page with special characters\n    // For now, we'll verify that the standard pages render valid JSON\n    await page.goto(\"/merchant-return-policy\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n\n    // Verify the JSON can be parsed without errors\n    expect(() => JSON.parse(jsonLdScript!)).not.toThrow();\n\n    // Verify no unescaped script tags in the output\n    expect(jsonLdScript).not.toContain(\"</script>\");\n  });\n});\n"
  },
  {
    "path": "tests/e2e/movieCarouselJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"MovieCarouselJsonLd\", () => {\n  test(\"renders summary page pattern with URLs only\", async ({ page }) => {\n    await page.goto(\"/movie-carousel-summary\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"ItemList\");\n    expect(jsonData.itemListElement).toHaveLength(5);\n\n    // Verify first item\n    expect(jsonData.itemListElement[0]).toEqual({\n      \"@type\": \"ListItem\",\n      position: 1,\n      url: \"https://example.com/movies/a-star-is-born\",\n    });\n\n    // Verify last item\n    expect(jsonData.itemListElement[4]).toEqual({\n      \"@type\": \"ListItem\",\n      position: 5,\n      url: \"https://example.com/movies/the-favourite\",\n    });\n  });\n\n  test(\"renders all-in-one page pattern with full movie data\", async ({\n    page,\n  }) => {\n    await page.goto(\"/movie-carousel\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"ItemList\");\n    expect(jsonData.itemListElement).toHaveLength(3);\n\n    // Verify first movie\n    const firstMovie = jsonData.itemListElement[0];\n    expect(firstMovie[\"@type\"]).toBe(\"ListItem\");\n    expect(firstMovie.position).toBe(1);\n    expect(firstMovie.item).toBeTruthy();\n    expect(firstMovie.item[\"@type\"]).toBe(\"Movie\");\n    expect(firstMovie.item.name).toBe(\"A Star Is Born\");\n    expect(firstMovie.item.image).toBe(\n      \"https://example.com/photos/6x9/star-is-born.jpg\",\n    );\n    expect(firstMovie.item.dateCreated).toBe(\"2024-10-05\");\n    expect(firstMovie.item.director).toEqual({\n      \"@type\": \"Person\",\n      name: \"Bradley Cooper\",\n    });\n\n    // Verify review\n    expect(firstMovie.item.review).toEqual({\n      \"@type\": \"Review\",\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 5,\n      },\n      author: {\n        \"@type\": \"Person\",\n        name: \"John D.\",\n      },\n    });\n\n    // Verify aggregate rating\n    expect(firstMovie.item.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 90,\n      bestRating: 100,\n      ratingCount: 19141,\n    });\n  });\n\n  test(\"renders advanced features including multiple images and complete data\", async ({\n    page,\n  }) => {\n    await page.goto(\"/movie-carousel-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify custom script ID\n    const scriptElement = await page.locator(\n      'script[type=\"application/ld+json\"]',\n    );\n    const scriptId = await scriptElement.getAttribute(\"id\");\n    expect(scriptId).toBe(\"movie-carousel-advanced\");\n\n    // Verify first movie with advanced features\n    const firstMovie = jsonData.itemListElement[0].item;\n    expect(firstMovie.name).toBe(\"Everything Everywhere All at Once\");\n    expect(firstMovie.url).toBe(\n      \"https://example.com/movies/everything-everywhere\",\n    );\n\n    // Verify multiple images with ImageObject\n    expect(Array.isArray(firstMovie.image)).toBe(true);\n    expect(firstMovie.image).toHaveLength(4);\n    expect(firstMovie.image[0]).toBe(\n      \"https://example.com/photos/1x1/eeaao.jpg\",\n    );\n    expect(firstMovie.image[1]).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/photos/4x3/eeaao.jpg\",\n      width: 1200,\n      height: 900,\n    });\n    expect(firstMovie.image[3]).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/photos/6x9/eeaao.jpg\",\n      width: 600,\n      height: 900,\n      caption: \"Official movie poster\",\n    });\n\n    // Verify director with URL\n    expect(firstMovie.director).toEqual({\n      \"@type\": \"Person\",\n      name: \"Daniel Kwan and Daniel Scheinert\",\n      url: \"https://example.com/directors/daniels\",\n    });\n\n    // Verify complete review with all properties\n    expect(firstMovie.review).toEqual({\n      \"@type\": \"Review\",\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 5,\n        bestRating: 5,\n        worstRating: 1,\n      },\n      author: {\n        \"@type\": \"Person\",\n        name: \"Sarah Johnson\",\n        url: \"https://example.com/reviewers/sarah-johnson\",\n      },\n      reviewBody:\n        \"A mind-bending masterpiece that explores the multiverse with heart, humor, and incredible creativity. The performances are outstanding.\",\n      datePublished: \"2024-03-30\",\n    });\n\n    // Verify comprehensive aggregate rating\n    expect(firstMovie.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 95,\n      bestRating: 100,\n      worstRating: 0,\n      ratingCount: 125432,\n      reviewCount: 8956,\n    });\n  });\n\n  test(\"processes different director formats correctly\", async ({ page }) => {\n    await page.goto(\"/movie-carousel-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // String director\n    const thirdMovie = jsonData.itemListElement[2].item;\n    expect(thirdMovie.director).toEqual({\n      \"@type\": \"Person\",\n      name: \"Joseph Kosinski\",\n    });\n\n    // Person object with additional properties\n    const secondMovie = jsonData.itemListElement[1].item;\n    expect(secondMovie.director).toEqual({\n      \"@type\": \"Person\",\n      name: \"Martin McDonagh\",\n      url: \"https://example.com/directors/martin-mcdonagh\",\n      familyName: \"McDonagh\",\n      givenName: \"Martin\",\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e/organizationJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"OrganizationJsonLd\", () => {\n  test(\"renders basic Organization structured data\", async ({ page }) => {\n    await page.goto(\"/organization\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.name).toBe(\"Example Corporation\");\n    expect(jsonData.url).toBe(\"https://www.example.com\");\n    expect(jsonData.logo).toBe(\"https://www.example.com/logo.png\");\n    expect(jsonData.description).toBe(\n      \"The example corporation is well-known for producing high-quality widgets\",\n    );\n    expect(jsonData.telephone).toBe(\"+1-999-999-9999\");\n    expect(jsonData.email).toBe(\"contact@example.com\");\n\n    // Verify sameAs is an array\n    expect(Array.isArray(jsonData.sameAs)).toBe(true);\n    expect(jsonData.sameAs).toHaveLength(3);\n    expect(jsonData.sameAs).toContain(\"https://twitter.com/example\");\n  });\n\n  test(\"renders OnlineStore with merchant return policy\", async ({ page }) => {\n    await page.goto(\"/online-store\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify OnlineStore type\n    expect(jsonData[\"@type\"]).toBe(\"OnlineStore\");\n    expect(jsonData.name).toBe(\"Example Online Store\");\n\n    // Verify logo as ImageObject\n    expect(jsonData.logo[\"@type\"]).toBe(\"ImageObject\");\n    expect(jsonData.logo.url).toBe(\"https://www.example.com/assets/logo.png\");\n    expect(jsonData.logo.width).toBe(600);\n    expect(jsonData.logo.height).toBe(400);\n\n    // Verify address\n    expect(jsonData.address[\"@type\"]).toBe(\"PostalAddress\");\n    expect(jsonData.address.streetAddress).toBe(\"999 W Example St Suite 99\");\n    expect(jsonData.address.addressLocality).toBe(\"New York\");\n    expect(jsonData.address.addressRegion).toBe(\"NY\");\n    expect(jsonData.address.postalCode).toBe(\"10019\");\n    expect(jsonData.address.addressCountry).toBe(\"US\");\n\n    // Verify contact point\n    expect(jsonData.contactPoint[\"@type\"]).toBe(\"ContactPoint\");\n    expect(jsonData.contactPoint.contactType).toBe(\"Customer Service\");\n    expect(jsonData.contactPoint.telephone).toBe(\"+1-999-999-9900\");\n    expect(jsonData.contactPoint.email).toBe(\"support@example.com\");\n\n    // Verify number of employees\n    expect(jsonData.numberOfEmployees[\"@type\"]).toBe(\"QuantitativeValue\");\n    expect(jsonData.numberOfEmployees.minValue).toBe(100);\n    expect(jsonData.numberOfEmployees.maxValue).toBe(999);\n\n    // Verify merchant return policy\n    expect(jsonData.hasMerchantReturnPolicy).toBeDefined();\n    expect(jsonData.hasMerchantReturnPolicy[\"@type\"]).toBe(\n      \"MerchantReturnPolicy\",\n    );\n    expect(jsonData.hasMerchantReturnPolicy.merchantReturnDays).toBe(60);\n    expect(jsonData.hasMerchantReturnPolicy.returnFees).toBe(\n      \"https://schema.org/FreeReturn\",\n    );\n\n    // Verify member program\n    expect(jsonData.hasMemberProgram).toBeDefined();\n    expect(jsonData.hasMemberProgram[\"@type\"]).toBe(\"MemberProgram\");\n    expect(jsonData.hasMemberProgram.name).toBe(\"Rewards Plus\");\n    expect(jsonData.hasMemberProgram.description).toContain(\"loyalty program\");\n\n    // Verify tiers\n    expect(Array.isArray(jsonData.hasMemberProgram.hasTiers)).toBe(true);\n    expect(jsonData.hasMemberProgram.hasTiers).toHaveLength(3);\n\n    // Bronze tier\n    expect(jsonData.hasMemberProgram.hasTiers[0][\"@type\"]).toBe(\n      \"MemberProgramTier\",\n    );\n    expect(jsonData.hasMemberProgram.hasTiers[0].name).toBe(\"Bronze\");\n    expect(jsonData.hasMemberProgram.hasTiers[0].hasTierBenefit).toBe(\n      \"https://schema.org/TierBenefitLoyaltyPoints\",\n    );\n    expect(\n      jsonData.hasMemberProgram.hasTiers[0].membershipPointsEarned,\n    ).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 1,\n    });\n\n    // Silver tier with MonetaryAmount requirement\n    expect(jsonData.hasMemberProgram.hasTiers[1].name).toBe(\"Silver\");\n    expect(jsonData.hasMemberProgram.hasTiers[1].hasTierRequirement).toEqual({\n      \"@type\": \"MonetaryAmount\",\n      value: 500,\n      currency: \"USD\",\n    });\n\n    // Gold tier with CreditCard requirement\n    expect(jsonData.hasMemberProgram.hasTiers[2].name).toBe(\"Gold\");\n    expect(jsonData.hasMemberProgram.hasTiers[2].hasTierRequirement).toEqual({\n      \"@type\": \"CreditCard\",\n      name: \"Example Gold Credit Card\",\n    });\n    expect(\n      jsonData.hasMemberProgram.hasTiers[2].membershipPointsEarned,\n    ).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 5,\n    });\n  });\n\n  test(\"renders OnlineStore with comprehensive loyalty programs\", async ({\n    page,\n  }) => {\n    await page.goto(\"/online-store-loyalty\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify OnlineStore type\n    expect(jsonData[\"@type\"]).toBe(\"OnlineStore\");\n    expect(jsonData.name).toBe(\"Premium Store\");\n\n    // Verify multiple member programs\n    expect(Array.isArray(jsonData.hasMemberProgram)).toBe(true);\n    expect(jsonData.hasMemberProgram).toHaveLength(2);\n\n    // Basic Rewards program\n    const basicProgram = jsonData.hasMemberProgram[0];\n    expect(basicProgram[\"@type\"]).toBe(\"MemberProgram\");\n    expect(basicProgram.name).toBe(\"Basic Rewards\");\n    expect(basicProgram.hasTiers).toHaveLength(2);\n\n    // Plus Member tier with UnitPriceSpecification requirement\n    const plusTier = basicProgram.hasTiers[1];\n    expect(plusTier.name).toBe(\"Plus Member\");\n    expect(plusTier.hasTierRequirement).toEqual({\n      \"@type\": \"UnitPriceSpecification\",\n      price: 4.99,\n      priceCurrency: \"USD\",\n      billingDuration: 12,\n      billingIncrement: 1,\n      unitCode: \"MON\",\n    });\n\n    // VIP Elite program\n    const vipProgram = jsonData.hasMemberProgram[1];\n    expect(vipProgram[\"@type\"]).toBe(\"MemberProgram\");\n    expect(vipProgram.name).toBe(\"VIP Elite Program\");\n    expect(vipProgram.hasTiers).toHaveLength(3);\n\n    // Silver VIP tier with @id\n    const silverTier = vipProgram.hasTiers[0];\n    expect(silverTier[\"@id\"]).toBe(\"#vip-silver\");\n    expect(silverTier.membershipPointsEarned).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 10,\n      unitText: \"points per dollar\",\n    });\n\n    // Gold VIP tier with complex QuantitativeValue\n    const goldTier = vipProgram.hasTiers[1];\n    expect(goldTier[\"@id\"]).toBe(\"#vip-gold\");\n    expect(goldTier.membershipPointsEarned).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 20,\n      minValue: 20,\n      maxValue: 40,\n      unitText: \"points per dollar (double on special events)\",\n    });\n\n    // Diamond VIP tier with text requirement\n    const diamondTier = vipProgram.hasTiers[2];\n    expect(diamondTier.name).toBe(\"Diamond VIP\");\n    expect(diamondTier.hasTierRequirement).toBe(\n      \"By invitation only - must maintain $10,000+ annual spending and participate in community events\",\n    );\n    expect(diamondTier.membershipPointsEarned).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 50,\n    });\n  });\n\n  test(\"renders Organization with review and aggregateRating\", async ({\n    page,\n  }) => {\n    await page.goto(\"/organization-reviews\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic info\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.name).toBe(\"Acme Software Inc.\");\n    expect(jsonData.url).toBe(\"https://www.acmesoftware.com\");\n\n    // Verify reviews\n    expect(Array.isArray(jsonData.review)).toBe(true);\n    expect(jsonData.review).toHaveLength(2);\n    expect(jsonData.review[0][\"@type\"]).toBe(\"Review\");\n    expect(jsonData.review[0].author[\"@type\"]).toBe(\"Person\");\n    expect(jsonData.review[0].author.name).toBe(\"Sarah Johnson\");\n    expect(jsonData.review[0].reviewRating[\"@type\"]).toBe(\"Rating\");\n    expect(jsonData.review[0].reviewRating.ratingValue).toBe(5);\n    expect(jsonData.review[0].datePublished).toBe(\"2025-06-15\");\n\n    expect(jsonData.review[1][\"@type\"]).toBe(\"Review\");\n    expect(jsonData.review[1].author[\"@type\"]).toBe(\"Person\");\n    expect(jsonData.review[1].author.name).toBe(\"Michael Chen\");\n    expect(jsonData.review[1].reviewRating.ratingValue).toBe(4);\n\n    // Verify aggregateRating\n    expect(jsonData.aggregateRating[\"@type\"]).toBe(\"AggregateRating\");\n    expect(jsonData.aggregateRating.ratingValue).toBe(4.6);\n    expect(jsonData.aggregateRating.ratingCount).toBe(312);\n    expect(jsonData.aggregateRating.reviewCount).toBe(245);\n    expect(jsonData.aggregateRating.bestRating).toBe(5);\n    expect(jsonData.aggregateRating.worstRating).toBe(1);\n  });\n\n  test(\"renders Organization with multiple addresses and contact points\", async ({\n    page,\n  }) => {\n    await page.goto(\"/organization-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic info\n    expect(jsonData[\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.name).toBe(\"Global Widget Corporation\");\n    expect(jsonData.alternateName).toBe(\"GWC\");\n    expect(jsonData.legalName).toBe(\"Global Widget Corporation Inc.\");\n    expect(jsonData.foundingDate).toBe(\"1995-03-15\");\n\n    // Verify multiple addresses\n    expect(Array.isArray(jsonData.address)).toBe(true);\n    expect(jsonData.address).toHaveLength(3);\n    expect(jsonData.address[0][\"@type\"]).toBe(\"PostalAddress\");\n    expect(jsonData.address[0].addressLocality).toBe(\"San Francisco\");\n    expect(jsonData.address[1].addressLocality).toBe(\"London\");\n    expect(jsonData.address[2].addressLocality).toBe(\"Tokyo\");\n\n    // Verify multiple contact points\n    expect(Array.isArray(jsonData.contactPoint)).toBe(true);\n    expect(jsonData.contactPoint).toHaveLength(3);\n    expect(jsonData.contactPoint[0].contactType).toBe(\"Customer Service\");\n    expect(jsonData.contactPoint[1].contactType).toBe(\"Sales\");\n    expect(jsonData.contactPoint[2].contactType).toBe(\"Technical Support\");\n\n    // Verify identifiers\n    expect(jsonData.taxID).toBe(\"98-7654321\");\n    expect(jsonData.vatID).toBe(\"GB123456789\");\n    expect(jsonData.duns).toBe(\"123456789\");\n    expect(jsonData.leiCode).toBe(\"529900T8BM49AURSDO55\");\n    expect(jsonData.naics).toBe(\"334111\");\n    expect(jsonData.globalLocationNumber).toBe(\"0614141000001\");\n    expect(jsonData.iso6523Code).toBe(\"0088:0614141000001\");\n  });\n});\n"
  },
  {
    "path": "tests/e2e/productJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"ProductJsonLd\", () => {\n  test(\"renders basic Product structured data\", async ({ page }) => {\n    await page.goto(\"/product\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify all properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Product\");\n    expect(jsonData.name).toBe(\"Executive Anvil\");\n    expect(jsonData.description).toBe(\n      \"Sleeker than ACME's Classic Anvil, the Executive Anvil is perfect for the business traveler looking for something to drop from a height.\",\n    );\n    expect(jsonData.url).toBe(\"https://example.com/products/anvil\");\n    expect(jsonData.sku).toBe(\"0446310786\");\n    expect(jsonData.mpn).toBe(\"925872\");\n\n    // Check brand\n    expect(jsonData.brand).toEqual({\n      \"@type\": \"Brand\",\n      name: \"ACME\",\n    });\n\n    // Check images\n    expect(jsonData.image).toEqual([\n      \"https://example.com/photos/1x1/photo.jpg\",\n      \"https://example.com/photos/4x3/photo.jpg\",\n      \"https://example.com/photos/16x9/photo.jpg\",\n    ]);\n\n    // Check offers\n    expect(jsonData.offers).toEqual({\n      \"@type\": \"Offer\",\n      price: 119.99,\n      priceCurrency: \"USD\",\n      availability: \"InStock\",\n      priceValidUntil: \"2024-12-31\",\n      url: \"https://example.com/buy/anvil\",\n    });\n\n    // Check aggregate rating\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.4,\n      reviewCount: 89,\n    });\n  });\n\n  test(\"renders Product with review containing pros and cons\", async ({\n    page,\n  }) => {\n    await page.goto(\"/product-review\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Product\");\n    expect(jsonData.name).toBe(\"Cheese Grater Pro\");\n\n    // Check review with pros and cons\n    expect(jsonData.review).toBeDefined();\n    expect(jsonData.review.name).toBe(\"Cheese Grater Pro Review\");\n    expect(jsonData.review.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Pascal Van Cleeff\",\n    });\n    expect(jsonData.review.reviewRating).toEqual({\n      \"@type\": \"Rating\",\n      ratingValue: 4,\n      bestRating: 5,\n    });\n\n    // Check positive notes (pros)\n    expect(jsonData.review.positiveNotes).toEqual({\n      \"@type\": \"ItemList\",\n      itemListElement: [\n        {\n          \"@type\": \"ListItem\",\n          position: 1,\n          name: \"Consistent results\",\n        },\n        {\n          \"@type\": \"ListItem\",\n          position: 2,\n          name: \"Still sharp after many uses\",\n        },\n        {\n          \"@type\": \"ListItem\",\n          position: 3,\n          name: \"Easy to clean\",\n        },\n        {\n          \"@type\": \"ListItem\",\n          position: 4,\n          name: \"Comfortable grip\",\n        },\n        {\n          \"@type\": \"ListItem\",\n          position: 5,\n          name: \"Multiple grating sizes\",\n        },\n      ],\n    });\n\n    // Check negative notes (cons)\n    expect(jsonData.review.negativeNotes).toEqual({\n      \"@type\": \"ItemList\",\n      itemListElement: [\n        {\n          \"@type\": \"ListItem\",\n          position: 1,\n          name: \"No child protection\",\n        },\n        {\n          \"@type\": \"ListItem\",\n          position: 2,\n          name: \"Lacking advanced features\",\n        },\n        {\n          \"@type\": \"ListItem\",\n          position: 3,\n          name: \"Takes up drawer space\",\n        },\n      ],\n    });\n  });\n\n  test(\"renders Product with AggregateOffer for shopping aggregator\", async ({\n    page,\n  }) => {\n    await page.goto(\"/product-aggregate\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Product\");\n    expect(jsonData.name).toBe(\"Executive Anvil\");\n\n    // Check AggregateOffer\n    expect(jsonData.offers).toEqual({\n      \"@type\": \"AggregateOffer\",\n      lowPrice: 119.99,\n      highPrice: 199.99,\n      priceCurrency: \"USD\",\n      offerCount: 5,\n    });\n\n    // Check multiple reviews\n    expect(Array.isArray(jsonData.review)).toBe(true);\n    expect(jsonData.review).toHaveLength(2);\n\n    expect(jsonData.review[0]).toEqual({\n      \"@type\": \"Review\",\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 5,\n        bestRating: 5,\n      },\n      author: {\n        \"@type\": \"Person\",\n        name: \"Fred Benson\",\n      },\n      reviewBody:\n        \"This anvil is perfect! Exactly what I needed for my roadrunner traps.\",\n      datePublished: \"2024-01-10\",\n    });\n\n    expect(jsonData.review[1]).toEqual({\n      \"@type\": \"Review\",\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 4,\n        bestRating: 5,\n      },\n      author: {\n        \"@type\": \"Person\",\n        name: \"Wile E. Coyote\",\n      },\n      reviewBody: \"Great anvil, but shipping took longer than expected.\",\n      datePublished: \"2024-01-05\",\n    });\n\n    // Check aggregate rating\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.4,\n      reviewCount: 89,\n    });\n  });\n\n  test(\"validates required properties\", async ({ page }) => {\n    await page.goto(\"/product\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Product must have name\n    expect(jsonData.name).toBeDefined();\n    expect(jsonData.name).not.toBe(\"\");\n\n    // Product must have at least one of: review, aggregateRating, or offers\n    const hasRequiredProperty =\n      jsonData.review !== undefined ||\n      jsonData.aggregateRating !== undefined ||\n      jsonData.offers !== undefined;\n\n    expect(hasRequiredProperty).toBe(true);\n  });\n\n  test(\"handles multiple images correctly\", async ({ page }) => {\n    await page.goto(\"/product\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(Array.isArray(jsonData.image)).toBe(true);\n    expect(jsonData.image).toHaveLength(3);\n    expect(jsonData.image).toContain(\n      \"https://example.com/photos/1x1/photo.jpg\",\n    );\n    expect(jsonData.image).toContain(\n      \"https://example.com/photos/4x3/photo.jpg\",\n    );\n    expect(jsonData.image).toContain(\n      \"https://example.com/photos/16x9/photo.jpg\",\n    );\n  });\n\n  test(\"properly formats offer with availability\", async ({ page }) => {\n    await page.goto(\"/product\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData.offers.availability).toBe(\"InStock\");\n    expect(jsonData.offers.priceValidUntil).toBe(\"2024-12-31\");\n    expect(jsonData.offers.url).toBe(\"https://example.com/buy/anvil\");\n  });\n\n  test(\"includes product identifiers\", async ({ page }) => {\n    await page.goto(\"/product\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData.sku).toBe(\"0446310786\");\n    expect(jsonData.mpn).toBe(\"925872\");\n  });\n});\n\ntest.describe(\"ProductJsonLd with ProductGroup\", () => {\n  test(\"renders ProductGroup with variants\", async ({ page }) => {\n    await page.goto(\"/product-variants\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify ProductGroup properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"ProductGroup\");\n    expect(jsonData.name).toBe(\"Wool Winter Coat\");\n    expect(jsonData.productGroupID).toBe(\"WC2024\");\n    expect(jsonData.brand).toEqual({\n      \"@type\": \"Brand\",\n      name: \"Nordic Style\",\n    });\n    expect(jsonData.material).toBe(\"wool\");\n    expect(jsonData.pattern).toBe(\"striped\");\n\n    // Verify variesBy\n    expect(jsonData.variesBy).toEqual([\n      \"https://schema.org/size\",\n      \"https://schema.org/color\",\n    ]);\n\n    // Verify hasVariant\n    expect(jsonData.hasVariant).toHaveLength(4);\n    expect(jsonData.hasVariant[0]).toMatchObject({\n      \"@type\": \"Product\",\n      name: \"Wool Winter Coat - Small Green\",\n      sku: \"WC2024-S-GRN\",\n      size: \"small\",\n      color: \"Green\",\n    });\n\n    // Check first variant offers\n    expect(jsonData.hasVariant[0].offers).toMatchObject({\n      \"@type\": \"Offer\",\n      price: 119.99,\n      priceCurrency: \"USD\",\n      availability: \"InStock\",\n    });\n\n    // Verify aggregateRating\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.6,\n      reviewCount: 127,\n    });\n  });\n\n  test(\"renders ProductGroup with advanced features\", async ({ page }) => {\n    await page.goto(\"/product-variants-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"ProductGroup\");\n    expect(jsonData.name).toBe(\"Athletic Performance Shoes\");\n\n    // Check brand object\n    expect(jsonData.brand).toEqual({\n      \"@type\": \"Organization\",\n      name: \"SpeedRunner Pro\",\n      logo: \"https://example.com/logos/speedrunner.png\",\n    });\n\n    // Check audience\n    expect(jsonData.audience).toEqual({\n      \"@type\": \"PeopleAudience\",\n      suggestedGender: \"unisex\",\n      suggestedAge: {\n        \"@type\": \"QuantitativeValue\",\n        minValue: 13,\n        unitCode: \"ANN\",\n      },\n    });\n\n    // Check reviews\n    expect(jsonData.review).toHaveLength(2);\n    expect(jsonData.review[0].name).toBe(\"Best Running Shoes Ever!\");\n    expect(jsonData.review[0].reviewRating).toEqual({\n      \"@type\": \"Rating\",\n      ratingValue: 5,\n      bestRating: 5,\n    });\n\n    // Check variants with mixed types\n    expect(jsonData.hasVariant).toBeDefined();\n    const fullVariant = jsonData.hasVariant[0];\n    expect(fullVariant.weight).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 310,\n      unitCode: \"GRM\",\n    });\n\n    // Check shipping details\n    expect(fullVariant.offers.shippingDetails).toBeDefined();\n    expect(fullVariant.offers.shippingDetails[\"@type\"]).toBe(\n      \"OfferShippingDetails\",\n    );\n\n    // Check URL-only variants\n    const urlVariant = jsonData.hasVariant.find(\n      (v: unknown) =>\n        (v as { url?: string }).url ===\n        \"https://example.com/products/athletic-performance-shoes/mens-9-gray\",\n    );\n    expect(urlVariant).toEqual({\n      url: \"https://example.com/products/athletic-performance-shoes/mens-9-gray\",\n    });\n  });\n\n  test(\"renders Product with isVariantOf reference\", async ({ page }) => {\n    await page.goto(\"/product-variants-multipage\");\n\n    // Get the ProductJsonLd script (first one)\n    const scripts = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .all();\n    expect(scripts.length).toBeGreaterThanOrEqual(1);\n\n    const productScript = await scripts[0].textContent();\n    const productData = JSON.parse(productScript!);\n\n    // Verify Product with isVariantOf\n    expect(productData[\"@type\"]).toBe(\"Product\");\n    expect(productData.name).toBe(\"Premium Leather Wallet - Brown Classic\");\n    expect(productData.sku).toBe(\"LW2024-BRN-CLS\");\n    expect(productData.color).toBe(\"Brown\");\n    expect(productData.pattern).toBe(\"Classic\");\n    expect(productData.size).toBe(\"Standard\");\n\n    // Check variant reference\n    expect(productData.isVariantOf).toEqual({ \"@id\": \"#wallet_group\" });\n    expect(productData.inProductGroupWithID).toBe(\"LW2024\");\n\n    // If there's a second script (the manually added ProductGroup), check it too\n    if (scripts.length > 1) {\n      const groupScript = await scripts[1].textContent();\n      const groupData = JSON.parse(groupScript!);\n\n      expect(groupData[\"@type\"]).toBe(\"ProductGroup\");\n      expect(groupData[\"@id\"]).toBe(\"#wallet_group\");\n      expect(groupData.productGroupID).toBe(\"LW2024\");\n      expect(groupData.variesBy).toContain(\"https://schema.org/color\");\n    }\n  });\n});\n"
  },
  {
    "path": "tests/e2e/profilePageJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"ProfilePageJsonLd\", () => {\n  test(\"renders basic Person profile structured data\", async ({ page }) => {\n    await page.goto(\"/profile\");\n\n    // Find the JSON-LD script tag\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic ProfilePage properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"ProfilePage\");\n    expect(jsonData.dateCreated).toBe(\"2024-12-23T12:34:00-05:00\");\n    expect(jsonData.dateModified).toBe(\"2024-12-26T14:53:00-05:00\");\n\n    // Verify mainEntity is correctly processed as Person\n    expect(jsonData.mainEntity).toEqual({\n      \"@type\": \"Person\",\n      name: \"Angelo Huff\",\n    });\n  });\n\n  test(\"renders advanced Person profile with all features\", async ({\n    page,\n  }) => {\n    await page.goto(\"/profile-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify ProfilePage structure\n    expect(jsonData[\"@type\"]).toBe(\"ProfilePage\");\n    expect(jsonData.dateCreated).toBe(\"2024-12-23T12:34:00-05:00\");\n    expect(jsonData.dateModified).toBe(\"2024-12-26T14:53:00-05:00\");\n\n    // Verify mainEntity with all Person properties\n    const mainEntity = jsonData.mainEntity;\n    expect(mainEntity[\"@type\"]).toBe(\"Person\");\n    expect(mainEntity.name).toBe(\"Angelo Huff\");\n    expect(mainEntity.alternateName).toBe(\"ahuff23\");\n    expect(mainEntity.identifier).toBe(\"123475623\");\n    expect(mainEntity.description).toBe(\"Defender of Truth\");\n    expect(mainEntity.image).toBe(\"https://example.com/avatars/ahuff23.jpg\");\n    expect(mainEntity.sameAs).toEqual([\n      \"https://www.example.com/real-angelo\",\n      \"https://example.com/profile/therealangelohuff\",\n    ]);\n\n    // Verify interaction statistics are properly processed\n    expect(mainEntity.interactionStatistic).toEqual([\n      {\n        \"@type\": \"InteractionCounter\",\n        interactionType: \"https://schema.org/FollowAction\",\n        userInteractionCount: 1,\n      },\n      {\n        \"@type\": \"InteractionCounter\",\n        interactionType: \"https://schema.org/LikeAction\",\n        userInteractionCount: 5,\n      },\n    ]);\n\n    expect(mainEntity.agentInteractionStatistic).toEqual({\n      \"@type\": \"InteractionCounter\",\n      interactionType: \"https://schema.org/WriteAction\",\n      userInteractionCount: 2346,\n    });\n  });\n\n  test(\"renders Organization profile structured data\", async ({ page }) => {\n    await page.goto(\"/profile-organization\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify ProfilePage structure\n    expect(jsonData[\"@type\"]).toBe(\"ProfilePage\");\n    expect(jsonData.dateCreated).toBe(\"2020-01-15T09:00:00-05:00\");\n    expect(jsonData.dateModified).toBe(\"2024-12-26T16:30:00-05:00\");\n\n    // Verify mainEntity is Organization\n    const mainEntity = jsonData.mainEntity;\n    expect(mainEntity[\"@type\"]).toBe(\"Organization\");\n    expect(mainEntity.name).toBe(\"TechForum Community\");\n    expect(mainEntity.url).toBe(\"https://techforum.example.com\");\n    expect(mainEntity.logo).toBe(\"https://techforum.example.com/logo.png\");\n    expect(mainEntity.alternateName).toBe(\"TechForum\");\n    expect(mainEntity.identifier).toBe(\"org-789012\");\n    expect(mainEntity.description).toBe(\n      \"A vibrant community for technology enthusiasts\",\n    );\n    expect(mainEntity.sameAs).toEqual([\n      \"https://twitter.com/techforum\",\n      \"https://linkedin.com/company/techforum\",\n      \"https://github.com/techforum\",\n    ]);\n\n    // Verify organization interaction statistics\n    expect(mainEntity.interactionStatistic).toEqual([\n      {\n        \"@type\": \"InteractionCounter\",\n        interactionType: \"https://schema.org/FollowAction\",\n        userInteractionCount: 15000,\n      },\n      {\n        \"@type\": \"InteractionCounter\",\n        interactionType: \"https://schema.org/LikeAction\",\n        userInteractionCount: 45000,\n      },\n    ]);\n\n    expect(mainEntity.agentInteractionStatistic).toEqual({\n      \"@type\": \"InteractionCounter\",\n      interactionType: \"https://schema.org/WriteAction\",\n      userInteractionCount: 8500,\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e/quizJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"QuizJsonLd\", () => {\n  test(\"renders basic Quiz structured data\", async ({ page }) => {\n    await page.goto(\"/quiz\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic structure\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org/\");\n    expect(jsonData[\"@type\"]).toBe(\"Quiz\");\n    expect(jsonData.hasPart).toHaveLength(3);\n\n    // Verify first question\n    expect(jsonData.hasPart[0]).toEqual({\n      \"@type\": \"Question\",\n      eduQuestionType: \"Flashcard\",\n      text: \"What is the capital of France?\",\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: \"Paris\",\n      },\n    });\n\n    // Verify second question\n    expect(jsonData.hasPart[1]).toEqual({\n      \"@type\": \"Question\",\n      eduQuestionType: \"Flashcard\",\n      text: \"What is 2 + 2?\",\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: \"4\",\n      },\n    });\n\n    // Verify third question\n    expect(jsonData.hasPart[2]).toEqual({\n      \"@type\": \"Question\",\n      eduQuestionType: \"Flashcard\",\n      text: \"Who wrote Romeo and Juliet?\",\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: \"William Shakespeare\",\n      },\n    });\n  });\n\n  test(\"renders Quiz with educational alignment\", async ({ page }) => {\n    await page.goto(\"/quiz-biology\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify about property\n    expect(jsonData.about).toEqual({\n      \"@type\": \"Thing\",\n      name: \"Cell Biology\",\n    });\n\n    // Verify educational alignment\n    expect(jsonData.educationalAlignment).toEqual([\n      {\n        \"@type\": \"AlignmentObject\",\n        alignmentType: \"educationalSubject\",\n        targetName: \"Biology\",\n      },\n      {\n        \"@type\": \"AlignmentObject\",\n        alignmentType: \"educationalLevel\",\n        targetName: \"Grade 10\",\n      },\n    ]);\n\n    // Verify questions exist\n    expect(jsonData.hasPart).toHaveLength(4);\n    expect(jsonData.hasPart[0].text).toBe(\n      \"What is the powerhouse of the cell?\",\n    );\n    expect(jsonData.hasPart[0].acceptedAnswer.text).toBe(\"Mitochondria\");\n  });\n\n  test(\"renders advanced Quiz with all features\", async ({ page }) => {\n    await page.goto(\"/quiz-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify script has custom ID\n    const script = await page.locator('script[type=\"application/ld+json\"]');\n    const scriptId = await script.getAttribute(\"id\");\n    expect(scriptId).toBe(\"advanced-quiz\");\n\n    // Verify about property with full Thing object\n    expect(jsonData.about).toEqual({\n      \"@type\": \"Thing\",\n      name: \"Earth and Space Science\",\n      description:\n        \"Fundamental concepts about Earth, space, and environmental systems\",\n      url: \"https://example.com/earth-science\",\n    });\n\n    // Verify all question formats\n    expect(jsonData.hasPart).toHaveLength(4);\n\n    // String format converted to question\n    expect(jsonData.hasPart[0]).toEqual({\n      \"@type\": \"Question\",\n      eduQuestionType: \"Flashcard\",\n      text: \"The Earth revolves around the Sun in approximately 365.25 days\",\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: \"The Earth revolves around the Sun in approximately 365.25 days\",\n      },\n    });\n\n    // Question/answer format\n    expect(jsonData.hasPart[1].text).toBe(\n      \"What is the chemical formula for water?\",\n    );\n    expect(jsonData.hasPart[1].acceptedAnswer.text).toBe(\"H2O\");\n\n    // Text/acceptedAnswer with string\n    expect(jsonData.hasPart[2].text).toBe(\"What causes tides on Earth?\");\n    expect(jsonData.hasPart[2].acceptedAnswer.text).toBe(\n      \"The gravitational pull of the Moon and Sun\",\n    );\n\n    // Text/acceptedAnswer with Answer object\n    expect(jsonData.hasPart[3].text).toBe(\"Explain the greenhouse effect\");\n    expect(jsonData.hasPart[3].acceptedAnswer).toEqual({\n      \"@type\": \"Answer\",\n      text: \"The greenhouse effect is a natural process where certain gases in Earth's atmosphere trap heat from the sun, warming the planet's surface\",\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e/recipeJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"RecipeJsonLd\", () => {\n  test(\"renders basic Recipe structured data\", async ({ page }) => {\n    await page.goto(\"/recipe\");\n\n    // Find the JSON-LD script tag\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic Recipe properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Recipe\");\n    expect(jsonData.name).toBe(\"Classic Chocolate Chip Cookies\");\n    expect(jsonData.image).toBe(\n      \"https://example.com/images/chocolate-chip-cookies.jpg\",\n    );\n    expect(jsonData.description).toBe(\n      \"The perfect chocolate chip cookies - crispy edges with soft, chewy centers\",\n    );\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Sarah Baker\",\n    });\n    expect(jsonData.datePublished).toBe(\"2024-01-20T09:00:00+00:00\");\n    expect(jsonData.url).toBe(\n      \"https://example.com/recipes/chocolate-chip-cookies\",\n    );\n    expect(jsonData.prepTime).toBe(\"PT20M\");\n    expect(jsonData.cookTime).toBe(\"PT12M\");\n    expect(jsonData.totalTime).toBe(\"PT32M\");\n    expect(jsonData.recipeYield).toBe(\"36 cookies\");\n    expect(jsonData.recipeCategory).toBe(\"dessert\");\n    expect(jsonData.recipeCuisine).toBe(\"American\");\n    expect(jsonData.keywords).toBe(\"cookies, chocolate chip, dessert, baking\");\n\n    // Verify ingredients array\n    expect(jsonData.recipeIngredient).toHaveLength(9);\n    expect(jsonData.recipeIngredient[0]).toBe(\"2 1/4 cups all-purpose flour\");\n    expect(jsonData.recipeIngredient[8]).toBe(\"2 cups chocolate chips\");\n\n    // Verify instructions array\n    expect(jsonData.recipeInstructions).toHaveLength(9);\n    expect(jsonData.recipeInstructions[0]).toBe(\"Preheat oven to 375°F\");\n    expect(jsonData.recipeInstructions[8]).toBe(\n      \"Cool on baking sheets for 2 minutes; remove to wire racks to cool completely\",\n    );\n  });\n\n  test(\"renders advanced Recipe with all features\", async ({ page }) => {\n    await page.goto(\"/recipe-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify Recipe type and name\n    expect(jsonData[\"@type\"]).toBe(\"Recipe\");\n    expect(jsonData.name).toBe(\"Authentic Italian Tiramisu\");\n\n    // Verify multiple images\n    expect(jsonData.image).toHaveLength(3);\n    expect(jsonData.image).toContain(\n      \"https://example.com/images/tiramisu-16x9.jpg\",\n    );\n    expect(jsonData.image).toContain(\n      \"https://example.com/images/tiramisu-4x3.jpg\",\n    );\n    expect(jsonData.image).toContain(\n      \"https://example.com/images/tiramisu-1x1.jpg\",\n    );\n\n    // Verify Organization author with logo\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Organization\",\n      name: \"La Cucina Italiana\",\n      url: \"https://example.com\",\n      logo: {\n        \"@type\": \"ImageObject\",\n        url: \"https://example.com/logo.png\",\n        width: 200,\n        height: 200,\n      },\n    });\n\n    // Verify durations\n    expect(jsonData.prepTime).toBe(\"PT30M\");\n    expect(jsonData.cookTime).toBe(\"PT0M\");\n    expect(jsonData.totalTime).toBe(\"PT4H30M\");\n\n    // Verify numeric yield\n    expect(jsonData.recipeYield).toBe(\"8 servings\");\n\n    // Verify HowToSection instructions\n    expect(jsonData.recipeInstructions).toHaveLength(2);\n    expect(jsonData.recipeInstructions[0][\"@type\"]).toBe(\"HowToSection\");\n    expect(jsonData.recipeInstructions[0].name).toBe(\n      \"Prepare the Mascarpone Cream\",\n    );\n    expect(jsonData.recipeInstructions[0].itemListElement).toHaveLength(4);\n    expect(jsonData.recipeInstructions[0].itemListElement[0]).toEqual({\n      \"@type\": \"HowToStep\",\n      text: \"Whisk egg yolks and sugar in a double boiler over simmering water until thick and pale (about 5 minutes)\",\n      image: \"https://example.com/images/tiramisu-step1.jpg\",\n    });\n\n    expect(jsonData.recipeInstructions[1][\"@type\"]).toBe(\"HowToSection\");\n    expect(jsonData.recipeInstructions[1].name).toBe(\"Assemble the Tiramisu\");\n    expect(jsonData.recipeInstructions[1].itemListElement).toHaveLength(6);\n\n    // Verify nutrition information\n    expect(jsonData.nutrition).toEqual({\n      \"@type\": \"NutritionInformation\",\n      calories: \"385 calories\",\n      proteinContent: \"7g\",\n      carbohydrateContent: \"28g\",\n      fatContent: \"28g\",\n      saturatedFatContent: \"16g\",\n      cholesterolContent: \"215mg\",\n      sodiumContent: \"95mg\",\n      sugarContent: \"20g\",\n      servingSize: \"1 piece (1/8 of dish)\",\n    });\n\n    // Verify aggregate rating\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.9,\n      ratingCount: 487,\n      reviewCount: 423,\n      bestRating: 5,\n      worstRating: 1,\n    });\n\n    // Verify video object\n    expect(jsonData.video).toEqual({\n      \"@type\": \"VideoObject\",\n      name: \"How to Make Authentic Tiramisu\",\n      description:\n        \"Watch our Italian chef demonstrate the traditional method of making tiramisu\",\n      thumbnailUrl: [\n        \"https://example.com/video/tiramisu-thumb-1.jpg\",\n        \"https://example.com/video/tiramisu-thumb-2.jpg\",\n      ],\n      contentUrl: \"https://example.com/videos/tiramisu-tutorial.mp4\",\n      embedUrl: \"https://example.com/embed/tiramisu-tutorial\",\n      uploadDate: \"2024-01-20T10:00:00+00:00\",\n      duration: \"PT12M45S\",\n    });\n  });\n\n  test(\"verifies all required properties are present\", async ({ page }) => {\n    await page.goto(\"/recipe\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify required properties according to Google's documentation\n    expect(jsonData).toHaveProperty(\"@context\");\n    expect(jsonData).toHaveProperty(\"@type\");\n    expect(jsonData).toHaveProperty(\"name\");\n    expect(jsonData).toHaveProperty(\"image\");\n\n    // Verify the values are not empty\n    expect(jsonData[\"@context\"]).toBeTruthy();\n    expect(jsonData[\"@type\"]).toBeTruthy();\n    expect(jsonData.name).toBeTruthy();\n    expect(jsonData.image).toBeTruthy();\n  });\n\n  test(\"renders multiple JSON-LD scripts on the same page\", async ({\n    page,\n  }) => {\n    // Navigate to a page with recipe structured data\n    await page.goto(\"/recipe\");\n\n    // Count JSON-LD scripts\n    const scriptsCount = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .count();\n    expect(scriptsCount).toBeGreaterThanOrEqual(1);\n\n    // Verify each script contains valid JSON\n    const scripts = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .all();\n    for (const script of scripts) {\n      const content = await script.textContent();\n      expect(() => JSON.parse(content!)).not.toThrow();\n\n      const jsonData = JSON.parse(content!);\n      // Verify it's a Recipe type\n      if (jsonData[\"@type\"] === \"Recipe\") {\n        expect(jsonData).toHaveProperty(\"name\");\n        expect(jsonData).toHaveProperty(\"image\");\n      }\n    }\n  });\n\n  test(\"verifies ISO 8601 duration format for time properties\", async ({\n    page,\n  }) => {\n    await page.goto(\"/recipe\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify ISO 8601 duration format\n    expect(jsonData.prepTime).toMatch(/^PT\\d+[HMS]/);\n    expect(jsonData.cookTime).toMatch(/^PT\\d+[HMS]/);\n    expect(jsonData.totalTime).toMatch(/^PT\\d+[HMS]/);\n\n    // Verify specific values\n    expect(jsonData.prepTime).toBe(\"PT20M\"); // 20 minutes\n    expect(jsonData.cookTime).toBe(\"PT12M\"); // 12 minutes\n    expect(jsonData.totalTime).toBe(\"PT32M\"); // 32 minutes total\n  });\n});\n"
  },
  {
    "path": "tests/e2e/reviewJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"ReviewJsonLd\", () => {\n  test(\"renders basic review structured data\", async ({ page }) => {\n    await page.goto(\"/review\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"Review\");\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Bob Smith\",\n    });\n    expect(jsonData.reviewRating).toEqual({\n      \"@type\": \"Rating\",\n      ratingValue: 4,\n    });\n    expect(jsonData.itemReviewed).toEqual({\n      \"@type\": \"LocalBusiness\",\n      name: \"Legal Seafood\",\n    });\n    expect(jsonData.reviewBody).toBe(\"Fresh seafood and great service!\");\n    expect(jsonData.datePublished).toBe(\"2024-01-01\");\n    expect(jsonData.url).toBe(\"/review\");\n  });\n\n  test(\"renders movie review with full details\", async ({ page }) => {\n    await page.goto(\"/review-movie\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Review\");\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Roger Ebert\",\n      url: \"https://example.com/reviewers/roger-ebert\",\n    });\n    expect(jsonData.reviewRating).toEqual({\n      \"@type\": \"Rating\",\n      ratingValue: 4,\n      bestRating: 4,\n      worstRating: 0,\n    });\n    expect(jsonData.itemReviewed).toMatchObject({\n      \"@type\": \"Movie\",\n      name: \"The Shawshank Redemption\",\n      director: \"Frank Darabont\",\n      actor: [\"Tim Robbins\", \"Morgan Freeman\"],\n    });\n    expect(jsonData.publisher).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Film Critics United\",\n      logo: \"https://example.com/fcu-logo.jpg\",\n    });\n    expect(jsonData.mainEntityOfPage).toBe(\n      \"https://example.com/reviews/shawshank-redemption\",\n    );\n  });\n\n  test(\"renders reviews nested in product\", async ({ page }) => {\n    await page.goto(\"/review-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"Product\");\n    expect(jsonData.name).toBe(\"The Catcher in the Rye\");\n\n    // Check nested reviews\n    expect(jsonData.review).toBeDefined();\n    expect(Array.isArray(jsonData.review)).toBe(true);\n    expect(jsonData.review.length).toBe(3);\n\n    // First review\n    expect(jsonData.review[0]).toMatchObject({\n      \"@type\": \"Review\",\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 5,\n      },\n      author: {\n        \"@type\": \"Person\",\n        name: \"John Doe\",\n      },\n      reviewBody: expect.stringContaining(\"timeless classic\"),\n      datePublished: \"2024-01-01\",\n    });\n\n    // Second review with author URL\n    expect(jsonData.review[1].author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Jane Smith\",\n      url: \"https://example.com/reviewers/jane\",\n    });\n\n    // Third review with organization author - now correctly detected as Organization\n    expect(jsonData.review[2].author).toEqual({\n      \"@type\": \"Organization\",\n      name: \"Literary Review Magazine\",\n    });\n\n    // Check aggregate rating\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.2,\n      bestRating: 5,\n      ratingCount: 150,\n      reviewCount: 120,\n    });\n  });\n\n  test(\"properly escapes special characters in review text\", async ({\n    page,\n  }) => {\n    // Create a test page with special characters\n    await page.goto(\"/review\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n\n    // Verify JSON is valid even with special content\n    expect(() => JSON.parse(jsonLdScript!)).not.toThrow();\n\n    // The stringify utility should handle dangerous sequences\n    // These are tested centrally in security.e2e.spec.ts\n  });\n});\n"
  },
  {
    "path": "tests/e2e/security.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"JSON-LD Security\", () => {\n  test(\"prevents script tag injection attacks\", async ({ page }) => {\n    // Create a simple test page\n    await page.setContent(`\n      <!DOCTYPE html>\n      <html>\n        <head>\n          <title>Security Test</title>\n        </head>\n        <body>\n          <div id=\"content\">Test Page</div>\n        </body>\n      </html>\n    `);\n\n    // Inject a script tag with malicious content attempts\n    await page.evaluate(() => {\n      const script = document.createElement(\"script\");\n      script.type = \"application/ld+json\";\n\n      const data = {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Article\",\n        headline: \"Normal title\",\n        description: 'Malicious </script><script>alert(\"XSS\")</script> attempt',\n        author: {\n          name: \"Author </SCRIPT><script>console.log('hacked')</script>\",\n          url: \"https://example.com/author?name=</script>&action=hack\",\n        },\n      };\n\n      // Simulate what our stringify function does\n      script.textContent = JSON.stringify(data)\n        .replace(/<\\/script>/gi, \"\\\\u003C/script>\") // Unicode escape for <\n        .replace(/<!--/g, \"\\\\u003C!--\") // Unicode escape for <\n        .replace(/-->/g, \"--\\\\u003E\"); // Unicode escape for >\n\n      document.head.appendChild(script);\n    });\n\n    // Verify no additional script tags were created\n    const scriptCount = await page.locator(\"script\").count();\n    expect(scriptCount).toBe(1); // Only our JSON-LD script\n\n    // Verify the JSON-LD content is properly escaped\n    const jsonLdContent = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdContent).toContain(\"\\\\u003C/script>\");\n    // Case-insensitive regex normalizes all to lowercase\n\n    // Verify no alerts were triggered\n    const alerts: string[] = [];\n    page.on(\"dialog\", (dialog) => {\n      alerts.push(dialog.message());\n      dialog.dismiss();\n    });\n\n    // Wait a bit to ensure no delayed scripts execute\n    await page.waitForTimeout(100);\n    expect(alerts).toHaveLength(0);\n\n    // Verify no console logs from injected scripts\n    const consoleLogs = await page.evaluate(() => {\n      return (\n        (window as Window & { consoleMessages?: string[] }).consoleMessages ||\n        []\n      );\n    });\n    expect(consoleLogs).not.toContain(\"hacked\");\n  });\n\n  test(\"prevents HTML comment injection\", async ({ page }) => {\n    await page.setContent(`\n      <!DOCTYPE html>\n      <html>\n        <head><title>Comment Test</title></head>\n        <body></body>\n      </html>\n    `);\n\n    await page.evaluate(() => {\n      const script = document.createElement(\"script\");\n      script.type = \"application/ld+json\";\n\n      const data = {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Recipe\",\n        name: \"Recipe with <!-- hidden --> content\",\n        description:\n          \"This has <!-- <script>alert('XSS')</script> --> hidden script\",\n        instructions: \"<!-- </script><script>alert('another')</script> -->\",\n      };\n\n      script.textContent = JSON.stringify(data)\n        .replace(/<\\/script>/gi, \"\\\\u003C/script>\") // Unicode escape for <\n        .replace(/<!--/g, \"\\\\u003C!--\") // Unicode escape for <\n        .replace(/-->/g, \"--\\\\u003E\"); // Unicode escape for >\n\n      document.head.appendChild(script);\n    });\n\n    // Verify comments are escaped\n    const jsonLdContent = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdContent).toContain(\"\\\\u003C!--\");\n    expect(jsonLdContent).toContain(\"--\\\\u003E\");\n    expect(jsonLdContent).not.toContain(\"<!--\");\n    expect(jsonLdContent).not.toContain(\"-->\");\n\n    // Verify the JSON can be parsed\n    const jsonData = await page.evaluate(() => {\n      const script = document.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      return JSON.parse(script!.textContent!);\n    });\n\n    expect(jsonData.name).toBe(\"Recipe with <!-- hidden --> content\");\n  });\n\n  test(\"handles edge cases safely\", async ({ page }) => {\n    await page.setContent(`\n      <!DOCTYPE html>\n      <html>\n        <head><title>Edge Case Test</title></head>\n        <body></body>\n      </html>\n    `);\n\n    await page.evaluate(() => {\n      const script = document.createElement(\"script\");\n      script.type = \"application/ld+json\";\n\n      const data = {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Article\",\n        // Multiple dangerous patterns in one string\n        headline: \"Article with </script><!-- comment --></SCRIPT> and more\",\n        // Nested escaping attempts\n        description: \"Nested: </<\\\\/script>script> attempt\",\n        // URL with dangerous sequences (edge case)\n        url: \"https://example.com/?q=</script>&c=<!--test-->\",\n        // Empty dangerous sequences\n        empty1: \"</script>\",\n        empty2: \"<!---->\",\n        // Mixed case\n        mixed: \"</ScRiPt> and </SCRIPT> and </script>\",\n      };\n\n      script.textContent = JSON.stringify(data)\n        .replace(/<\\/script>/gi, \"\\\\u003C/script>\") // Unicode escape for <\n        .replace(/<!--/g, \"\\\\u003C!--\") // Unicode escape for <\n        .replace(/-->/g, \"--\\\\u003E\"); // Unicode escape for >\n\n      document.head.appendChild(script);\n    });\n\n    const jsonLdContent = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n\n    // Verify all dangerous sequences are escaped\n    expect(jsonLdContent).not.toMatch(/<\\/script/i);\n    expect(jsonLdContent).not.toContain(\"<!--\");\n    expect(jsonLdContent).not.toContain(\"-->\");\n\n    // Verify JSON parsing still works\n    const jsonData = await page.evaluate(() => {\n      const script = document.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      return JSON.parse(script!.textContent!);\n    });\n\n    expect(jsonData.headline).toBe(\n      \"Article with </script><!-- comment --></script> and more\",\n    );\n    expect(jsonData.url).toBe(\"https://example.com/?q=</script>&c=<!--test-->\");\n  });\n\n  test(\"verifies safe rendering in Next.js-like environment\", async ({\n    page,\n  }) => {\n    // Simulate server-side rendered content with our JSON-LD\n    const htmlContent = `\n      <!DOCTYPE html>\n      <html>\n        <head>\n          <title>Next.js-like Test</title>\n          <script type=\"application/ld+json\">${\n            JSON.stringify({\n              \"@context\": \"https://schema.org\",\n              \"@type\": \"WebPage\",\n              name: \"Page with </script> in title\",\n              url: \"https://example.com/page?utm_source=google&utm_medium=cpc&utm_campaign=test\",\n              description: \"This has <!-- comments --> and </SCRIPT> tags\",\n            })\n              .replace(/<\\/script>/gi, \"\\\\u003C/script>\") // Unicode escape for <\n              .replace(/<!--/g, \"\\\\u003C!--\") // Unicode escape for <\n              .replace(/-->/g, \"--\\\\u003E\") // Unicode escape for >\n          }</script>\n        </head>\n        <body>\n          <h1>Test Page</h1>\n        </body>\n      </html>\n    `;\n\n    await page.setContent(htmlContent);\n\n    // Verify page loads without executing injected scripts\n    const title = await page.title();\n    expect(title).toBe(\"Next.js-like Test\");\n\n    // Verify only one script tag exists\n    const scriptCount = await page.locator(\"script\").count();\n    expect(scriptCount).toBe(1);\n\n    // Verify JSON-LD data integrity\n    const jsonData = await page.evaluate(() => {\n      const script = document.querySelector(\n        'script[type=\"application/ld+json\"]',\n      );\n      return JSON.parse(script!.textContent!);\n    });\n\n    expect(jsonData.name).toBe(\"Page with </script> in title\");\n    expect(jsonData.url).toBe(\n      \"https://example.com/page?utm_source=google&utm_medium=cpc&utm_campaign=test\",\n    );\n    expect(jsonData.description).toBe(\n      \"This has <!-- comments --> and </script> tags\",\n    );\n  });\n});\n"
  },
  {
    "path": "tests/e2e/softwareApplicationJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"SoftwareApplicationJsonLd\", () => {\n  test(\"renders basic free software app structured data\", async ({ page }) => {\n    await page.goto(\"/software-app\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify all properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"SoftwareApplication\");\n    expect(jsonData.name).toBe(\"Task Master Pro\");\n    expect(jsonData.description).toBe(\n      \"A powerful task management app to boost your productivity\",\n    );\n    expect(jsonData.applicationCategory).toBe(\"ProductivityApplication\");\n    expect(jsonData.operatingSystem).toBe(\n      \"Windows 10+, macOS 10.15+, Ubuntu 20.04+\",\n    );\n\n    // Verify offers\n    expect(jsonData.offers).toEqual({\n      \"@type\": \"Offer\",\n      price: 0,\n      priceCurrency: \"USD\",\n    });\n\n    // Verify aggregate rating\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.5,\n      ratingCount: 1250,\n      reviewCount: 980,\n    });\n\n    // Verify screenshots\n    expect(jsonData.screenshot).toHaveLength(3);\n    expect(jsonData.screenshot[0]).toBe(\n      \"https://example.com/screenshots/dashboard.jpg\",\n    );\n\n    // Verify feature list\n    expect(jsonData.featureList).toEqual([\n      \"Intuitive task organization\",\n      \"Calendar integration\",\n      \"Team collaboration\",\n      \"Progress tracking\",\n      \"Mobile sync\",\n    ]);\n\n    // Verify dates\n    expect(jsonData.datePublished).toBe(\"2022-06-15\");\n    expect(jsonData.dateModified).toBe(\"2024-11-28\");\n  });\n\n  test(\"renders paid software app with pricing tiers\", async ({ page }) => {\n    await page.goto(\"/software-app-paid\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"DesignApplication\");\n    expect(jsonData.name).toBe(\"Studio Pro - Advanced Photo Editor\");\n    expect(jsonData.applicationSubCategory).toBe(\"PhotoEditing\");\n    expect(jsonData.applicationSuite).toBe(\"Creative Studio Suite\");\n\n    // Verify paid offers\n    expect(jsonData.offers).toEqual({\n      \"@type\": \"Offer\",\n      price: 79.99,\n      priceCurrency: \"USD\",\n      availability: \"https://schema.org/InStock\",\n      validFrom: \"2024-01-01\",\n    });\n\n    // Verify reviews\n    expect(jsonData.review).toHaveLength(2);\n    expect(jsonData.review[0]).toMatchObject({\n      \"@type\": \"Review\",\n      author: {\n        \"@type\": \"Person\",\n        name: \"Sarah Johnson\",\n      },\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 5,\n        bestRating: 5,\n      },\n      reviewBody:\n        \"Best photo editing software I've ever used. The AI features save me hours of work!\",\n      datePublished: \"2024-10-15\",\n    });\n\n    // Verify system requirements\n    expect(jsonData.memoryRequirements).toBe(\n      \"8GB RAM minimum, 16GB recommended\",\n    );\n    expect(jsonData.processorRequirements).toBe(\n      \"Intel Core i5 or AMD Ryzen 5 or better\",\n    );\n    expect(jsonData.storageRequirements).toBe(\"4GB available space\");\n\n    // Verify countries supported\n    expect(jsonData.countriesSupported).toEqual([\n      \"US\",\n      \"CA\",\n      \"GB\",\n      \"AU\",\n      \"NZ\",\n      \"IE\",\n    ]);\n  });\n\n  test(\"renders mobile application with permissions\", async ({ page }) => {\n    await page.goto(\"/mobile-app\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"MobileApplication\");\n    expect(jsonData.name).toBe(\"FitTrack - Fitness & Workout Tracker\");\n    expect(jsonData.applicationCategory).toBe(\"HealthApplication\");\n    expect(jsonData.operatingSystem).toBe(\"Android 7.0+, iOS 13.0+\");\n\n    // Verify permissions\n    expect(jsonData.permissions).toEqual([\n      \"android.permission.ACTIVITY_RECOGNITION\",\n      \"android.permission.ACCESS_FINE_LOCATION\",\n      \"android.permission.CAMERA\",\n      \"android.permission.READ_EXTERNAL_STORAGE\",\n      \"android.permission.RECEIVE_BOOT_COMPLETED\",\n    ]);\n\n    // Verify multiple images\n    expect(jsonData.image).toHaveLength(3);\n    expect(jsonData.image[0]).toBe(\"https://example.com/fittrack-icon-1x1.png\");\n    expect(jsonData.image[1]).toEqual({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/fittrack-icon-4x3.png\",\n      width: 1200,\n      height: 900,\n    });\n\n    // Verify download URLs\n    expect(jsonData.downloadUrl).toBe(\n      \"https://play.google.com/store/apps/details?id=com.fittech.fittrack\",\n    );\n    expect(jsonData.installUrl).toBe(\n      \"https://apps.apple.com/app/fittrack/id123456789\",\n    );\n  });\n\n  test(\"renders web application with multiple pricing tiers\", async ({\n    page,\n  }) => {\n    await page.goto(\"/web-app\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@type\"]).toBe(\"WebApplication\");\n    expect(jsonData.name).toBe(\"CloudSync Pro - Team Collaboration Platform\");\n    expect(jsonData.url).toBe(\"https://app.cloudsyncpro.com\");\n\n    // Verify multiple offers\n    expect(jsonData.offers).toHaveLength(3);\n    expect(jsonData.offers[0]).toEqual({\n      \"@type\": \"Offer\",\n      price: 0,\n      priceCurrency: \"USD\",\n      availability: \"https://schema.org/InStock\",\n      url: \"https://app.cloudsyncpro.com/signup/free\",\n    });\n    expect(jsonData.offers[1].price).toBe(12);\n    expect(jsonData.offers[2].price).toBe(25);\n\n    // Verify publisher with address\n    expect(jsonData.publisher).toMatchObject({\n      \"@type\": \"Organization\",\n      name: \"CloudSync Technologies Inc.\",\n      url: \"https://cloudsynctech.com\",\n      logo: {\n        \"@type\": \"ImageObject\",\n        url: \"https://cloudsynctech.com/press/logo.png\",\n        width: 600,\n        height: 60,\n      },\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"123 Tech Boulevard\",\n        addressLocality: \"San Francisco\",\n        addressRegion: \"CA\",\n        postalCode: \"94105\",\n        addressCountry: \"US\",\n      },\n    });\n\n    // Verify extensive country support\n    expect(jsonData.countriesSupported).toHaveLength(20);\n    expect(jsonData.countriesSupported).toContain(\"US\");\n    expect(jsonData.countriesSupported).toContain(\"JP\");\n  });\n\n  test(\"renders video game with co-typing\", async ({ page }) => {\n    await page.goto(\"/video-game\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify co-typed @type\n    expect(jsonData[\"@type\"]).toEqual([\"VideoGame\", \"MobileApplication\"]);\n    expect(jsonData.name).toBe(\"Dragon Quest Legends\");\n    expect(jsonData.applicationCategory).toBe(\"GameApplication\");\n    expect(jsonData.applicationSubCategory).toBe(\"RolePlaying\");\n\n    // Verify gaming-specific features\n    expect(jsonData.operatingSystem).toBe(\n      \"iOS 14.0+, Android 9.0+, Nintendo Switch\",\n    );\n    expect(jsonData.offers.price).toBe(19.99);\n\n    // Verify high ratings for games\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.8,\n      ratingCount: 45000,\n      reviewCount: 35000,\n      bestRating: 5,\n    });\n\n    // Verify game-specific feature list\n    expect(jsonData.featureList).toContain(\"50+ hours of main storyline\");\n    expect(jsonData.featureList).toContain(\"Multiplayer raids and PvP battles\");\n    expect(jsonData.featureList).toContain(\"300+ unique monsters to collect\");\n  });\n\n  test(\"properly formats all data types\", async ({ page }) => {\n    await page.goto(\"/software-app-paid\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n\n    // Verify JSON is valid\n    expect(() => JSON.parse(jsonLdScript!)).not.toThrow();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify image object processing\n    expect(jsonData.image[\"@type\"]).toBe(\"ImageObject\");\n    expect(jsonData.image.url).toBe(\"https://example.com/studio-pro-icon.png\");\n\n    // Verify author processing (string to Person)\n    expect(jsonData.author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Creative Software Labs\",\n      url: \"https://creativesoftwarelabs.com\",\n    });\n\n    // Verify publisher logo processing\n    expect(jsonData.publisher.logo[\"@type\"]).toBe(\"ImageObject\");\n\n    // Verify screenshot caption processing\n    expect(jsonData.screenshot[0]).toMatchObject({\n      \"@type\": \"ImageObject\",\n      url: \"https://example.com/screenshots/studio-pro-main.jpg\",\n      caption: \"Main editing interface\",\n    });\n  });\n\n  test(\"uses correct scriptKey for different app types\", async ({ page }) => {\n    // Test basic SoftwareApplication\n    await page.goto(\"/software-app\");\n    let script = await page.locator('script[type=\"application/ld+json\"]');\n    await expect(script).toHaveAttribute(\n      \"id\",\n      /software-application-jsonld-softwareapplication/,\n    );\n\n    // Test MobileApplication\n    await page.goto(\"/mobile-app\");\n    script = await page.locator('script[type=\"application/ld+json\"]');\n    await expect(script).toHaveAttribute(\n      \"id\",\n      /software-application-jsonld-mobileapplication/,\n    );\n\n    // Test WebApplication\n    await page.goto(\"/web-app\");\n    script = await page.locator('script[type=\"application/ld+json\"]');\n    await expect(script).toHaveAttribute(\n      \"id\",\n      /software-application-jsonld-webapplication/,\n    );\n\n    // Test VideoGame co-typed\n    await page.goto(\"/video-game\");\n    script = await page.locator('script[type=\"application/ld+json\"]');\n    await expect(script).toHaveAttribute(\n      \"id\",\n      /software-application-jsonld-videogame-mobileapplication/,\n    );\n  });\n});\n"
  },
  {
    "path": "tests/e2e/vacationRentalJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"VacationRentalJsonLd\", () => {\n  test(\"renders basic VacationRental structured data\", async ({ page }) => {\n    await page.goto(\"/vacation-rental\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify all required properties\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"VacationRental\");\n    expect(jsonData.name).toBe(\"Beautiful Beach House\");\n    expect(jsonData.identifier).toBe(\"beach-house-123\");\n    expect(jsonData.latitude).toBe(42.12345);\n    expect(jsonData.longitude).toBe(-71.98765);\n\n    // Verify containsPlace\n    expect(jsonData.containsPlace).toBeTruthy();\n    expect(jsonData.containsPlace[\"@type\"]).toBe(\"Accommodation\");\n    expect(jsonData.containsPlace.occupancy).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 5,\n    });\n\n    // Verify image (should be an array even with single image)\n    expect(Array.isArray(jsonData.image)).toBe(true);\n    expect(jsonData.image).toHaveLength(1);\n    expect(jsonData.image[0]).toBe(\n      \"https://example.com/vacation-rental-main.jpg\",\n    );\n  });\n\n  test(\"renders advanced VacationRental with all features\", async ({\n    page,\n  }) => {\n    await page.goto(\"/vacation-rental-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify basic properties\n    expect(jsonData[\"@type\"]).toBe(\"VacationRental\");\n    expect(jsonData.name).toBe(\"Luxury Ocean View Villa\");\n    expect(jsonData.identifier).toBe(\"lux-villa-malibu-456\");\n    expect(jsonData.additionalType).toBe(\"Villa\");\n\n    // Verify address\n    expect(jsonData.address).toEqual({\n      \"@type\": \"PostalAddress\",\n      addressCountry: \"US\",\n      addressLocality: \"Malibu\",\n      addressRegion: \"California\",\n      postalCode: \"90265\",\n      streetAddress: \"123 Ocean Drive, Unit 6E\",\n    });\n\n    // Verify containsPlace with all features\n    const containsPlace = jsonData.containsPlace;\n    expect(containsPlace[\"@type\"]).toBe(\"Accommodation\");\n    expect(containsPlace.additionalType).toBe(\"EntirePlace\");\n    expect(containsPlace.numberOfBedrooms).toBe(3);\n    expect(containsPlace.numberOfBathroomsTotal).toBe(2.5);\n    expect(containsPlace.numberOfRooms).toBe(7);\n    expect(containsPlace.petsAllowed).toBe(true);\n    expect(containsPlace.smokingAllowed).toBe(false);\n\n    // Verify bed details\n    expect(Array.isArray(containsPlace.bed)).toBe(true);\n    expect(containsPlace.bed).toHaveLength(2);\n    expect(containsPlace.bed[0]).toEqual({\n      \"@type\": \"BedDetails\",\n      numberOfBeds: 1,\n      typeOfBed: \"Queen\",\n    });\n\n    // Verify amenity features\n    expect(Array.isArray(containsPlace.amenityFeature)).toBe(true);\n    const acFeature = containsPlace.amenityFeature.find(\n      (f: { name: string }) => f.name === \"ac\",\n    );\n    expect(acFeature).toEqual({\n      \"@type\": \"LocationFeatureSpecification\",\n      name: \"ac\",\n      value: true,\n    });\n    const poolTypeFeature = containsPlace.amenityFeature.find(\n      (f: { name: string }) => f.name === \"poolType\",\n    );\n    expect(poolTypeFeature).toEqual({\n      \"@type\": \"LocationFeatureSpecification\",\n      name: \"poolType\",\n      value: \"Outdoor\",\n    });\n\n    // Verify floor size\n    expect(containsPlace.floorSize).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 150,\n      unitCode: \"MTK\",\n    });\n\n    // Verify images array\n    expect(Array.isArray(jsonData.image)).toBe(true);\n    expect(jsonData.image).toHaveLength(8);\n\n    // Verify aggregate rating\n    expect(jsonData.aggregateRating).toEqual({\n      \"@type\": \"AggregateRating\",\n      ratingValue: 4.8,\n      ratingCount: 125,\n      reviewCount: 98,\n      bestRating: 5,\n    });\n\n    // Verify brand\n    expect(jsonData.brand).toEqual({\n      \"@type\": \"Brand\",\n      name: \"Luxury Beach Rentals Inc\",\n    });\n\n    // Verify check-in/out times\n    expect(jsonData.checkinTime).toBe(\"15:00:00-08:00\");\n    expect(jsonData.checkoutTime).toBe(\"11:00:00-08:00\");\n\n    // Verify languages\n    expect(jsonData.knowsLanguage).toEqual([\"en-US\", \"es-ES\", \"fr-FR\"]);\n\n    // Verify reviews\n    expect(Array.isArray(jsonData.review)).toBe(true);\n    expect(jsonData.review).toHaveLength(3);\n    expect(jsonData.review[0].author).toEqual({\n      \"@type\": \"Person\",\n      name: \"Sarah Johnson\",\n    });\n  });\n\n  test(\"renders apartment type VacationRental\", async ({ page }) => {\n    await page.goto(\"/vacation-rental-apartment\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Verify apartment-specific properties\n    expect(jsonData[\"@type\"]).toBe(\"VacationRental\");\n    expect(jsonData.additionalType).toBe(\"Apartment\");\n    expect(jsonData.name).toBe(\"Cozy Manhattan Studio Apartment\");\n\n    // Verify private room accommodation\n    const containsPlace = jsonData.containsPlace;\n    expect(containsPlace.additionalType).toBe(\"PrivateRoom\");\n    expect(containsPlace.occupancy.value).toBe(2);\n\n    // Verify single bed (not array)\n    expect(containsPlace.bed).toEqual({\n      \"@type\": \"BedDetails\",\n      numberOfBeds: 1,\n      typeOfBed: \"Double\",\n    });\n\n    // Verify floor size in square feet\n    expect(containsPlace.floorSize).toEqual({\n      \"@type\": \"QuantitativeValue\",\n      value: 450,\n      unitCode: \"FTK\",\n    });\n\n    // Verify string coordinates\n    expect(jsonData.latitude).toBe(\"40.74844\");\n    expect(jsonData.longitude).toBe(\"-73.98566\");\n\n    // Verify single language (converted to array)\n    expect(jsonData.knowsLanguage).toEqual([\"en-US\"]);\n\n    // Verify single review (not array)\n    expect(jsonData.review).toMatchObject({\n      \"@type\": \"Review\",\n      reviewRating: {\n        \"@type\": \"Rating\",\n        ratingValue: 4,\n      },\n      author: {\n        \"@type\": \"Person\",\n        name: \"David Lee\",\n      },\n    });\n  });\n\n  test(\"validates JSON structure is properly escaped\", async ({ page }) => {\n    await page.goto(\"/vacation-rental\");\n\n    const scriptElement = await page.locator(\n      'script[type=\"application/ld+json\"]',\n    );\n    const scriptContent = await scriptElement.textContent();\n\n    // Ensure the JSON can be parsed without errors\n    expect(() => JSON.parse(scriptContent!)).not.toThrow();\n\n    // Check that the script tag has proper attributes\n    const scriptId = await scriptElement.getAttribute(\"id\");\n\n    expect(scriptId).toBe(\"vacationrental-jsonld\");\n  });\n});\n"
  },
  {
    "path": "tests/e2e/videoJsonLd.e2e.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\ntest.describe(\"VideoJsonLd\", () => {\n  test(\"renders basic video structured data\", async ({ page }) => {\n    await page.goto(\"/video\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    expect(jsonLdScript).toBeTruthy();\n\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData[\"@context\"]).toBe(\"https://schema.org\");\n    expect(jsonData[\"@type\"]).toBe(\"VideoObject\");\n    expect(jsonData.name).toBe(\"How to Make a Perfect Chocolate Cake\");\n    expect(jsonData.description).toBe(\n      \"Learn how to make the perfect chocolate cake with this easy step-by-step recipe tutorial\",\n    );\n    expect(jsonData.thumbnailUrl).toBe(\n      \"https://example.com/chocolate-cake-thumbnail.jpg\",\n    );\n    expect(jsonData.uploadDate).toBe(\"2024-01-15T08:00:00+00:00\");\n    expect(jsonData.contentUrl).toBe(\n      \"https://example.com/videos/chocolate-cake-recipe.mp4\",\n    );\n    expect(jsonData.embedUrl).toBe(\n      \"https://example.com/embed/chocolate-cake-recipe\",\n    );\n    expect(jsonData.duration).toBe(\"PT10M30S\");\n  });\n\n  test(\"renders advanced video with all features\", async ({ page }) => {\n    await page.goto(\"/video-advanced\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    // Check multiple thumbnails\n    expect(jsonData.thumbnailUrl).toHaveLength(3);\n    expect(jsonData.thumbnailUrl[0]).toBe(\n      \"https://example.com/thumbnails/masterclass-1x1.jpg\",\n    );\n\n    // Check interaction statistics\n    expect(jsonData.interactionStatistic).toHaveLength(2);\n    expect(jsonData.interactionStatistic[0][\"@type\"]).toBe(\n      \"InteractionCounter\",\n    );\n    expect(jsonData.interactionStatistic[0].interactionType).toBe(\n      \"WatchAction\",\n    );\n    expect(jsonData.interactionStatistic[0].userInteractionCount).toBe(500000);\n\n    // Check regions\n    expect(jsonData.regionsAllowed).toEqual([\"US\", \"CA\", \"GB\", \"AU\", \"NZ\"]);\n    expect(jsonData.ineligibleRegion).toEqual([\"CN\", \"RU\"]);\n\n    // Check authors\n    expect(jsonData.author).toHaveLength(2);\n    expect(jsonData.author[0][\"@type\"]).toBe(\"Person\");\n    expect(jsonData.author[0].name).toBe(\"Chef Julia Martinez\");\n    expect(jsonData.author[1].name).toBe(\"Chef Paul Anderson\");\n\n    // Check publisher\n    expect(jsonData.publisher[\"@type\"]).toBe(\"Organization\");\n    expect(jsonData.publisher.name).toBe(\"Culinary Institute Online\");\n    expect(jsonData.publisher.logo).toBe(\n      \"https://example.com/culinary-institute-logo.png\",\n    );\n  });\n\n  test(\"renders live video with BroadcastEvent\", async ({ page }) => {\n    await page.goto(\"/video-live\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData.publication).toHaveLength(2);\n\n    // Check first broadcast\n    expect(jsonData.publication[0][\"@type\"]).toBe(\"BroadcastEvent\");\n    expect(jsonData.publication[0].name).toBe(\"First Broadcast\");\n    expect(jsonData.publication[0].isLiveBroadcast).toBe(true);\n    expect(jsonData.publication[0].startDate).toBe(\"2024-12-31T20:00:00+00:00\");\n    expect(jsonData.publication[0].endDate).toBe(\"2024-12-31T22:00:00+00:00\");\n\n    // Check encore broadcast\n    expect(jsonData.publication[1][\"@type\"]).toBe(\"BroadcastEvent\");\n    expect(jsonData.publication[1].name).toBe(\"Encore Presentation\");\n    expect(jsonData.publication[1].isLiveBroadcast).toBe(true);\n    expect(jsonData.publication[1].startDate).toBe(\"2025-01-01T14:00:00+00:00\");\n  });\n\n  test(\"renders video with clips for key moments\", async ({ page }) => {\n    await page.goto(\"/video-clips\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData.hasPart).toHaveLength(5);\n\n    // Check first clip\n    expect(jsonData.hasPart[0][\"@type\"]).toBe(\"Clip\");\n    expect(jsonData.hasPart[0].name).toBe(\"Introduction to French Pastries\");\n    expect(jsonData.hasPart[0].startOffset).toBe(0);\n    expect(jsonData.hasPart[0].endOffset).toBe(180);\n    expect(jsonData.hasPart[0].url).toBe(\n      \"https://example.com/videos/french-pastries-guide?t=0\",\n    );\n\n    // Check croissants clip\n    expect(jsonData.hasPart[1].name).toBe(\"Making Croissants\");\n    expect(jsonData.hasPart[1].startOffset).toBe(180);\n    expect(jsonData.hasPart[1].endOffset).toBe(720);\n\n    // Verify all clips have proper structure\n    jsonData.hasPart.forEach(\n      (clip: {\n        \"@type\": string;\n        name: string;\n        startOffset: number;\n        endOffset: number;\n        url: string;\n      }) => {\n        expect(clip[\"@type\"]).toBe(\"Clip\");\n        expect(clip.name).toBeTruthy();\n        expect(typeof clip.startOffset).toBe(\"number\");\n        expect(typeof clip.endOffset).toBe(\"number\");\n        expect(clip.url).toMatch(/\\?t=\\d+$/);\n      },\n    );\n  });\n\n  test(\"renders video with SeekToAction\", async ({ page }) => {\n    await page.goto(\"/video-seekto\");\n\n    const jsonLdScript = await page\n      .locator('script[type=\"application/ld+json\"]')\n      .textContent();\n    const jsonData = JSON.parse(jsonLdScript!);\n\n    expect(jsonData.potentialAction).toBeTruthy();\n    expect(jsonData.potentialAction[\"@type\"]).toBe(\"SeekToAction\");\n    expect(jsonData.potentialAction.target).toBe(\n      \"https://example.com/videos/kitchen-tips?t={seek_to_second_number}\",\n    );\n    expect(jsonData.potentialAction[\"startOffset-input\"]).toBe(\n      \"required name=seek_to_second_number\",\n    );\n  });\n});\n"
  },
  {
    "path": "tests/unit/setup.ts",
    "content": "import \"@testing-library/jest-dom\"; // Provides custom DOM matchers\n"
  },
  {
    "path": "tsconfig.json",
    "content": "// next-seo/tsconfig.json\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"DOM\", \"ESNext\"],\n    \"jsx\": \"react-jsx\",\n    \"declaration\": true,\n    \"outDir\": \"./dist\",\n    \"strict\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./src/*\"]\n    },\n    \"types\": [\"@testing-library/jest-dom\"]\n  },\n  \"include\": [\n    \"src/**/*.ts\",\n    \"src/**/*.tsx\",\n    \"tests/**/*.ts\",\n    \"playwright.config.ts\",\n    \"vitest.config.ts\",\n    \"tsup.config.ts\"\n  ],\n  \"exclude\": [\"node_modules\", \"dist\", \"examples\"]\n}\n"
  },
  {
    "path": "tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\nimport path from \"path\";\n\nexport default defineConfig({\n  entry: {\n    index: \"src/index.ts\",\n    pages: \"src/pages/index.ts\",\n  },\n  format: [\"cjs\", \"esm\"],\n  splitting: false,\n  sourcemap: true,\n  clean: true,\n  dts: true,\n  external: [\"react\"],\n  esbuildOptions(options) {\n    options.alias = {\n      \"~\": path.resolve(__dirname, \"./src\"),\n    };\n  },\n});\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\nimport react from \"@vitejs/plugin-react\";\nimport path from \"path\";\n\nexport default defineConfig({\n  plugins: [react()],\n  test: {\n    globals: true,\n    environment: \"jsdom\",\n    setupFiles: \"./tests/unit/setup.ts\",\n    include: [\n      \"src/**/*.{test,spec}.{js,ts,jsx,tsx}\",\n      \"tests/unit/**/*.{test,spec}.{js,ts,jsx,tsx}\",\n    ],\n    exclude: [\n      \"node_modules\",\n      \"dist\",\n      \".idea\",\n      \".git\",\n      \".cache\",\n      \"coverage\",\n      \"examples\",\n      \"tests/e2e\",\n      \"playwright-report\",\n      \"repomix-output.xml\",\n    ],\n    coverage: {\n      provider: \"v8\",\n      reporter: [\"text\", \"json\", \"html\"],\n      reportsDirectory: \"./coverage/unit\",\n      exclude: [\n        \"node_modules\",\n        \"dist\",\n        \"examples\",\n        \"*.spec.ts\",\n        \"*.config.*\",\n        \"tests\",\n        \"src/index.ts\",\n      ],\n    },\n  },\n  resolve: {\n    alias: {\n      \"~\": path.resolve(__dirname, \"./src\"),\n    },\n  },\n});\n"
  }
]