Repository: garmeeh/next-seo
Branch: main
Commit: f74b86bbeaa4
Files: 286
Total size: 1.8 MB
Directory structure:
gitextract_boozz7yy/
├── .changeset/
│ ├── README.md
│ └── config.json
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── dependabot.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── changeset-check.yml
│ ├── changesets.yml
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .husky/
│ └── pre-commit
├── .npmignore
├── .prettierignore
├── .vscode/
│ └── settings.json
├── ADDING_NEW_COMPONENTS.md
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── CUSTOM_COMPONENTS.md
├── LICENSE
├── LICENSE.md
├── LIST.md
├── README.md
├── eslint.config.mjs
├── examples/
│ └── app-router-showcase/
│ ├── .gitignore
│ ├── CLAUDE.md
│ ├── README.md
│ ├── app/
│ │ ├── aggregate-rating/
│ │ │ └── page.tsx
│ │ ├── aggregate-rating-restaurant/
│ │ │ └── page.tsx
│ │ ├── article/
│ │ │ └── page.tsx
│ │ ├── blog-posting/
│ │ │ └── page.tsx
│ │ ├── breadcrumb/
│ │ │ ├── advanced/
│ │ │ │ └── page.tsx
│ │ │ ├── multiple/
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── carousel-course/
│ │ │ └── page.tsx
│ │ ├── carousel-movie/
│ │ │ └── page.tsx
│ │ ├── carousel-recipe/
│ │ │ └── page.tsx
│ │ ├── carousel-restaurant/
│ │ │ └── page.tsx
│ │ ├── carousel-summary/
│ │ │ └── page.tsx
│ │ ├── claim-review/
│ │ │ └── page.tsx
│ │ ├── claim-review-advanced/
│ │ │ └── page.tsx
│ │ ├── claim-review-organization/
│ │ │ └── page.tsx
│ │ ├── course/
│ │ │ └── page.tsx
│ │ ├── course-list/
│ │ │ └── page.tsx
│ │ ├── course-list-summary/
│ │ │ └── page.tsx
│ │ ├── creative-work/
│ │ │ └── page.tsx
│ │ ├── creative-work-blog/
│ │ │ └── page.tsx
│ │ ├── creative-work-multiple/
│ │ │ └── page.tsx
│ │ ├── creative-work-news/
│ │ │ └── page.tsx
│ │ ├── custom-podcast/
│ │ │ └── page.tsx
│ │ ├── custom-service/
│ │ │ └── page.tsx
│ │ ├── dataset/
│ │ │ └── page.tsx
│ │ ├── dataset-advanced/
│ │ │ └── page.tsx
│ │ ├── dataset-catalog/
│ │ │ └── page.tsx
│ │ ├── dataset-nested/
│ │ │ └── page.tsx
│ │ ├── discussion-forum/
│ │ │ └── page.tsx
│ │ ├── discussion-forum-advanced/
│ │ │ └── page.tsx
│ │ ├── discussion-forum-deleted/
│ │ │ └── page.tsx
│ │ ├── employer-aggregate-rating/
│ │ │ └── page.tsx
│ │ ├── employer-aggregate-rating-advanced/
│ │ │ └── page.tsx
│ │ ├── employer-aggregate-rating-custom-scale/
│ │ │ └── page.tsx
│ │ ├── event/
│ │ │ └── page.tsx
│ │ ├── event-cancelled/
│ │ │ └── page.tsx
│ │ ├── event-free/
│ │ │ └── page.tsx
│ │ ├── event-rescheduled/
│ │ │ └── page.tsx
│ │ ├── faq/
│ │ │ └── page.tsx
│ │ ├── faq-advanced/
│ │ │ └── page.tsx
│ │ ├── faq-health/
│ │ │ └── page.tsx
│ │ ├── globals.css
│ │ ├── howto/
│ │ │ └── page.tsx
│ │ ├── howto-advanced/
│ │ │ └── page.tsx
│ │ ├── image/
│ │ │ └── page.tsx
│ │ ├── image-advanced/
│ │ │ └── page.tsx
│ │ ├── image-multiple/
│ │ │ └── page.tsx
│ │ ├── job-posting/
│ │ │ └── page.tsx
│ │ ├── job-posting-advanced/
│ │ │ └── page.tsx
│ │ ├── job-posting-remote/
│ │ │ └── page.tsx
│ │ ├── jsonld-test-page/
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── local-business/
│ │ │ └── page.tsx
│ │ ├── merchant-return-policy/
│ │ │ └── page.tsx
│ │ ├── merchant-return-policy-advanced/
│ │ │ └── page.tsx
│ │ ├── merchant-return-policy-link/
│ │ │ └── page.tsx
│ │ ├── mobile-app/
│ │ │ └── page.tsx
│ │ ├── movie-carousel/
│ │ │ └── page.tsx
│ │ ├── movie-carousel-advanced/
│ │ │ └── page.tsx
│ │ ├── movie-carousel-summary/
│ │ │ └── page.tsx
│ │ ├── news-article/
│ │ │ └── page.tsx
│ │ ├── online-store/
│ │ │ └── page.tsx
│ │ ├── online-store-loyalty/
│ │ │ └── page.tsx
│ │ ├── organization/
│ │ │ └── page.tsx
│ │ ├── organization-advanced/
│ │ │ └── page.tsx
│ │ ├── organization-reviews/
│ │ │ └── page.tsx
│ │ ├── page.module.css
│ │ ├── page.tsx
│ │ ├── product/
│ │ │ └── page.tsx
│ │ ├── product-3d-model/
│ │ │ └── page.tsx
│ │ ├── product-aggregate/
│ │ │ └── page.tsx
│ │ ├── product-certification/
│ │ │ └── page.tsx
│ │ ├── product-member-pricing/
│ │ │ └── page.tsx
│ │ ├── product-review/
│ │ │ └── page.tsx
│ │ ├── product-sale-pricing/
│ │ │ └── page.tsx
│ │ ├── product-shipping-options/
│ │ │ └── page.tsx
│ │ ├── product-unit-pricing/
│ │ │ └── page.tsx
│ │ ├── product-variants/
│ │ │ └── page.tsx
│ │ ├── product-variants-advanced/
│ │ │ └── page.tsx
│ │ ├── product-variants-multipage/
│ │ │ └── page.tsx
│ │ ├── product-with-return-policy/
│ │ │ └── page.tsx
│ │ ├── profile/
│ │ │ └── page.tsx
│ │ ├── profile-advanced/
│ │ │ └── page.tsx
│ │ ├── profile-organization/
│ │ │ └── page.tsx
│ │ ├── quiz/
│ │ │ └── page.tsx
│ │ ├── quiz-advanced/
│ │ │ └── page.tsx
│ │ ├── quiz-biology/
│ │ │ └── page.tsx
│ │ ├── recipe/
│ │ │ └── page.tsx
│ │ ├── recipe-advanced/
│ │ │ └── page.tsx
│ │ ├── restaurant/
│ │ │ └── page.tsx
│ │ ├── review/
│ │ │ └── page.tsx
│ │ ├── review-advanced/
│ │ │ └── page.tsx
│ │ ├── review-movie/
│ │ │ └── page.tsx
│ │ ├── social-media-posting/
│ │ │ └── page.tsx
│ │ ├── software-app/
│ │ │ └── page.tsx
│ │ ├── software-app-paid/
│ │ │ └── page.tsx
│ │ ├── store-with-departments/
│ │ │ └── page.tsx
│ │ ├── test-arrays/
│ │ │ └── page.tsx
│ │ ├── test-nested/
│ │ │ └── page.tsx
│ │ ├── test-url-params/
│ │ │ └── page.tsx
│ │ ├── vacation-rental/
│ │ │ └── page.tsx
│ │ ├── vacation-rental-advanced/
│ │ │ └── page.tsx
│ │ ├── vacation-rental-apartment/
│ │ │ └── page.tsx
│ │ ├── video/
│ │ │ └── page.tsx
│ │ ├── video-advanced/
│ │ │ └── page.tsx
│ │ ├── video-clips/
│ │ │ └── page.tsx
│ │ ├── video-game/
│ │ │ └── page.tsx
│ │ ├── video-live/
│ │ │ └── page.tsx
│ │ ├── video-seekto/
│ │ │ └── page.tsx
│ │ └── web-app/
│ │ └── page.tsx
│ ├── components/
│ │ └── custom/
│ │ ├── PodcastSeriesJsonLd.tsx
│ │ └── ServiceJsonLd.tsx
│ ├── eslint.config.mjs
│ ├── next.config.ts
│ ├── package.json
│ └── tsconfig.json
├── package.json
├── playwright.config.ts
├── pnpm-workspace.yaml
├── repomix.config.json
├── src/
│ ├── components/
│ │ ├── .gitkeep
│ │ ├── AggregateRatingJsonLd.test.tsx
│ │ ├── AggregateRatingJsonLd.tsx
│ │ ├── ArticleJsonLd.test.tsx
│ │ ├── ArticleJsonLd.tsx
│ │ ├── BreadcrumbJsonLd.test.tsx
│ │ ├── BreadcrumbJsonLd.tsx
│ │ ├── CLAUDE.md
│ │ ├── CarouselJsonLd.test.tsx
│ │ ├── CarouselJsonLd.tsx
│ │ ├── ClaimReviewJsonLd.test.tsx
│ │ ├── ClaimReviewJsonLd.tsx
│ │ ├── CourseJsonLd.test.tsx
│ │ ├── CourseJsonLd.tsx
│ │ ├── CreativeWorkJsonLd.test.tsx
│ │ ├── CreativeWorkJsonLd.tsx
│ │ ├── DatasetJsonLd.test.tsx
│ │ ├── DatasetJsonLd.tsx
│ │ ├── DiscussionForumPostingJsonLd.test.tsx
│ │ ├── DiscussionForumPostingJsonLd.tsx
│ │ ├── EmployerAggregateRatingJsonLd.test.tsx
│ │ ├── EmployerAggregateRatingJsonLd.tsx
│ │ ├── EventJsonLd.test.tsx
│ │ ├── EventJsonLd.tsx
│ │ ├── FAQJsonLd.test.tsx
│ │ ├── FAQJsonLd.tsx
│ │ ├── HowToJsonLd.test.tsx
│ │ ├── HowToJsonLd.tsx
│ │ ├── ImageJsonLd.test.tsx
│ │ ├── ImageJsonLd.tsx
│ │ ├── JobPostingJsonLd.test.tsx
│ │ ├── JobPostingJsonLd.tsx
│ │ ├── LocalBusinessJsonLd.test.tsx
│ │ ├── LocalBusinessJsonLd.tsx
│ │ ├── MerchantReturnPolicyJsonLd.test.tsx
│ │ ├── MerchantReturnPolicyJsonLd.tsx
│ │ ├── MovieCarouselJsonLd.test.tsx
│ │ ├── MovieCarouselJsonLd.tsx
│ │ ├── OrganizationJsonLd.test.tsx
│ │ ├── OrganizationJsonLd.tsx
│ │ ├── ProductJsonLd.test.tsx
│ │ ├── ProductJsonLd.tsx
│ │ ├── ProfilePageJsonLd.test.tsx
│ │ ├── ProfilePageJsonLd.tsx
│ │ ├── QuizJsonLd.test.tsx
│ │ ├── QuizJsonLd.tsx
│ │ ├── RecipeJsonLd.test.tsx
│ │ ├── RecipeJsonLd.tsx
│ │ ├── ReviewJsonLd.test.tsx
│ │ ├── ReviewJsonLd.tsx
│ │ ├── SoftwareApplicationJsonLd.test.tsx
│ │ ├── SoftwareApplicationJsonLd.tsx
│ │ ├── VacationRentalJsonLd.test.tsx
│ │ ├── VacationRentalJsonLd.tsx
│ │ ├── VideoJsonLd.test.tsx
│ │ └── VideoJsonLd.tsx
│ ├── core/
│ │ ├── JsonLdScript.test.tsx
│ │ └── JsonLdScript.tsx
│ ├── index.ts
│ ├── pages/
│ │ ├── README.md
│ │ ├── core/
│ │ │ ├── __snapshots__/
│ │ │ │ └── buildTags.test.tsx.snap
│ │ │ ├── buildTags.test.tsx
│ │ │ └── buildTags.tsx
│ │ ├── index.ts
│ │ ├── types/
│ │ │ └── index.ts
│ │ └── utils/
│ │ └── processors.ts
│ ├── types/
│ │ ├── article.types.ts
│ │ ├── breadcrumb.types.ts
│ │ ├── carousel.types.ts
│ │ ├── claimreview.types.ts
│ │ ├── common.types.ts
│ │ ├── course.types.ts
│ │ ├── creativework.types.ts
│ │ ├── dataset.types.ts
│ │ ├── discussionforum.types.ts
│ │ ├── employer-aggregate-rating.types.ts
│ │ ├── event.types.ts
│ │ ├── faq.types.ts
│ │ ├── howto.types.ts
│ │ ├── image.types.ts
│ │ ├── index.ts
│ │ ├── jobposting.types.ts
│ │ ├── localbusiness.types.ts
│ │ ├── merchantreturnpolicy.types.ts
│ │ ├── movie-carousel.types.ts
│ │ ├── organization.types.ts
│ │ ├── product.types.ts
│ │ ├── profile.types.ts
│ │ ├── quiz.types.ts
│ │ ├── recipe.types.ts
│ │ ├── review.types.ts
│ │ ├── softwareApplication.types.ts
│ │ ├── vacationrental.types.ts
│ │ └── video.types.ts
│ └── utils/
│ ├── processors.export.ts
│ ├── processors.test.ts
│ ├── processors.ts
│ ├── stringify.test.ts
│ └── stringify.ts
├── tests/
│ ├── e2e/
│ │ ├── .gitkeep
│ │ ├── CLAUDE.md
│ │ ├── aggregateRatingJsonLd.e2e.spec.ts
│ │ ├── articleJsonLd.e2e.spec.ts
│ │ ├── breadcrumbJsonLd.e2e.spec.ts
│ │ ├── carouselJsonLd.e2e.spec.ts
│ │ ├── claimReviewJsonLd.e2e.spec.ts
│ │ ├── courseJsonLd.e2e.spec.ts
│ │ ├── creativeWorkJsonLd.e2e.spec.ts
│ │ ├── customComponents.e2e.spec.ts
│ │ ├── datasetJsonLd.e2e.spec.ts
│ │ ├── discussionForumPostingJsonLd.e2e.spec.ts
│ │ ├── employerAggregateRatingJsonLd.e2e.spec.ts
│ │ ├── eventJsonLd.e2e.spec.ts
│ │ ├── faqJsonLd.e2e.spec.ts
│ │ ├── howtoJsonLd.e2e.spec.ts
│ │ ├── imageJsonLd.e2e.spec.ts
│ │ ├── jobPostingJsonLd.e2e.spec.ts
│ │ ├── jsonLdScript.e2e.spec.ts
│ │ ├── jsonValidation.e2e.spec.ts
│ │ ├── localBusinessJsonLd.e2e.spec.ts
│ │ ├── merchantReturnPolicyJsonLd.e2e.spec.ts
│ │ ├── movieCarouselJsonLd.e2e.spec.ts
│ │ ├── organizationJsonLd.e2e.spec.ts
│ │ ├── productJsonLd.e2e.spec.ts
│ │ ├── profilePageJsonLd.e2e.spec.ts
│ │ ├── quizJsonLd.e2e.spec.ts
│ │ ├── recipeJsonLd.e2e.spec.ts
│ │ ├── reviewJsonLd.e2e.spec.ts
│ │ ├── security.e2e.spec.ts
│ │ ├── softwareApplicationJsonLd.e2e.spec.ts
│ │ ├── vacationRentalJsonLd.e2e.spec.ts
│ │ └── videoJsonLd.e2e.spec.ts
│ └── unit/
│ └── setup.ts
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .changeset/README.md
================================================
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
================================================
FILE: .changeset/config.json
================================================
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["app-router-showcase"]
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: ""
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is.
**Reproduction**
For issues to be triaged in a timely manner please provide a Codesandbox/Github of the issue in it's simplest reproduction.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: ""
assignees: ""
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
Any links to Google or http://schema.org/ to support to validity in terms of SEO will be a great help.
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore(deps)"
open-pull-requests-limit: 5
================================================
FILE: .github/pull_request_template.md
================================================
================================================
FILE: .github/workflows/changeset-check.yml
================================================
name: Changeset Check
on:
pull_request:
types: [opened, synchronize]
jobs:
changeset-check:
name: Check for Changeset
runs-on: ubuntu-latest
if: github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'github-actions[bot]'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check for code changes
id: code-changes
run: |
# Get list of changed files
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}..HEAD)
# Check if any code files were changed (not just docs/config)
CODE_CHANGED=false
for file in $CHANGED_FILES; do
# Check if file is a code file (not docs, config, or github workflows)
if [[ "$file" =~ \.(ts|tsx|js|jsx)$ ]] && [[ ! "$file" =~ ^\.github/ ]] && [[ ! "$file" =~ \.(md|mdx)$ ]]; then
CODE_CHANGED=true
break
fi
done
echo "code_changed=$CODE_CHANGED" >> $GITHUB_OUTPUT
- name: Check for changeset
if: steps.code-changes.outputs.code_changed == 'true'
run: |
pnpm changeset status --since=origin/${{ github.base_ref }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Comment on PR if changeset is missing
if: failure() && steps.code-changes.outputs.code_changed == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const body = `## 📝 Changeset Required
This PR includes code changes but is missing a changeset. Changesets help us:
- Track changes for release notes
- Determine version bumps (patch/minor/major)
- Credit contributors properly
### How to add a changeset:
1. Run \`pnpm changeset\` in your local environment
2. Select the type of change (patch/minor/major)
3. Write a brief description of your changes
4. Commit the generated changeset file
### Change types:
- **patch**: Bug fixes, internal changes (0.0.X)
- **minor**: New features, non-breaking changes (0.X.0)
- **major**: Breaking changes (X.0.0)
### Example:
\`\`\`bash
$ pnpm changeset
? What kind of change is this? › patch
? Summary › Fixed TypeScript types for ArticleJsonLd component
\`\`\`
If this PR only contains documentation or non-code changes, you can ignore this message.`;
// Check if we already commented
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const hasComment = comments.data.some(comment =>
comment.body.includes('## 📝 Changeset Required')
);
if (!hasComment) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
}
- name: Skip message for non-code changes
if: steps.code-changes.outputs.code_changed == 'false'
run: echo "No code changes detected, changeset not required."
================================================
FILE: .github/workflows/changesets.yml
================================================
name: Changesets
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
build:
name: Build Library
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build library
run: pnpm build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist
retention-days: 1
lint:
name: Lint Library
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ESLint
run: pnpm lint
typecheck:
name: Type Check Library
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run TypeScript type checking
run: pnpm typecheck
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run unit tests
run: pnpm test:unit
example-lint:
name: Lint Example App
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint example app
run: pnpm example:lint
example-typecheck:
name: Type Check Example App
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist
- name: Type check example app
run: pnpm example:typecheck
e2e-tests:
name: E2E Tests
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist
- name: Build example app
run: pnpm example:build
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: pnpm test:e2e --project=chromium
env:
CI: true
release:
name: Create Release PR or Publish
runs-on: ubuntu-latest
needs:
[
build,
lint,
typecheck,
unit-tests,
example-lint,
example-typecheck,
e2e-tests,
]
outputs:
published: ${{ steps.changesets.outputs.published }}
publishedPackages: ${{ steps.changesets.outputs.publishedPackages }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
with:
title: "Release: Version Packages"
commit: "chore: version packages"
publish: pnpm release
createGithubReleases: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
post-release:
name: Post Release Actions
runs-on: ubuntu-latest
needs: release
if: needs.release.outputs.published == 'true'
steps:
- name: Report Released Packages
run: |
echo "🎉 Released packages:"
echo "${{ needs.release.outputs.publishedPackages }}"
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build Library
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build library
run: pnpm build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist
retention-days: 1
lint:
name: Lint Library
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ESLint
run: pnpm lint
typecheck:
name: Type Check Library
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run TypeScript type checking
run: pnpm typecheck
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run unit tests
run: pnpm test:unit
- name: Upload coverage reports
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-report
path: coverage/
retention-days: 7
example-lint:
name: Lint Example App
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint example app
run: pnpm example:lint
example-typecheck:
name: Type Check Example App
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist
- name: Type check example app
run: pnpm example:typecheck
e2e-tests:
name: E2E Tests
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist
- name: Build example app
run: pnpm example:build
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: pnpm test:e2e --project=chromium
env:
CI: true
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
all-checks:
name: All CI Checks
runs-on: ubuntu-latest
needs:
[
build,
lint,
typecheck,
unit-tests,
example-lint,
example-typecheck,
e2e-tests,
]
if: always()
steps:
- name: Check all job statuses
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "One or more CI checks failed"
exit 1
elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more CI checks were cancelled"
exit 1
else
echo "All CI checks passed successfully"
fi
================================================
FILE: .github/workflows/release.yml
================================================
name: Manual Release
on:
workflow_dispatch:
inputs:
version:
description: "Version to release (e.g., 7.1.0)"
required: true
type: string
tag:
description: "NPM tag (latest, next, alpha, beta)"
required: true
type: choice
default: "latest"
options:
- latest
- next
- alpha
- beta
permissions:
contents: write
id-token: write
jobs:
manual-release:
name: Manual Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Update package version
run: |
npm version ${{ github.event.inputs.version }} --no-git-tag-version
echo "Updated package.json to version ${{ github.event.inputs.version }}"
- name: Build package
run: pnpm build
- name: Run TypeScript type checking
run: pnpm typecheck
- name: Run linting
run: pnpm lint
- name: Run unit tests
run: pnpm test:unit
- name: Lint example app
run: pnpm example:lint
- name: Type check example app
run: pnpm example:typecheck
- name: Build example app
run: pnpm example:build
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: pnpm test:e2e --project=chromium
env:
CI: true
- name: Publish to npm
run: |
echo "Publishing version ${{ github.event.inputs.version }} with tag '${{ github.event.inputs.tag }}'"
pnpm publish --tag ${{ github.event.inputs.tag }} --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create Git tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "v${{ github.event.inputs.version }}" -m "Release v${{ github.event.inputs.version }}"
git push origin "v${{ github.event.inputs.version }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: "v${{ github.event.inputs.version }}"
name: "v${{ github.event.inputs.version }}"
body: |
## Manual Release v${{ github.event.inputs.version }}
Published to npm with tag: `${{ github.event.inputs.tag }}`
Install with:
```bash
npm install next-seo@${{ github.event.inputs.tag }}
# or
pnpm add next-seo@${{ github.event.inputs.tag }}
```
prerelease: ${{ github.event.inputs.tag != 'latest' }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Dependencies
node_modules
/.pnp
.pnp.js
# Testing
coverage
# Next.js
.next/
build/
out/
# TypeScript
*.tsbuildinfo
# Misc
.DS_Store
*.pem
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.sublime-workspace
# Husky
.husky/_/
# SWC
.swc/
*.log
.history
dist
cypress/videos
cypress/screenshots
package-lock.json
lib
notes.md
playwright-report
repomix-output.xml
.last-run.json
.claude
tasks/*
test-results/*
task.md
local_docs/*
================================================
FILE: .husky/pre-commit
================================================
pnpm lint-staged
================================================
FILE: .npmignore
================================================
# Source code
src/
examples/
# Config files
.husky/
.github/
.vscode/
.editorconfig
.prettierignore
commitlint.config.js
eslint.config.mjs
playwright.config.ts
tsconfig.json
tsup.config.ts
vitest.config.ts
pnpm-workspace.yaml
repomix.config.json
# Documentation
*.md
!README.md
# Test files
tests/
test-results/
coverage/
**/*.test.*
**/*.spec.*
# Development files
tasks/
schemas/
# Build artifacts that shouldn't be published
*.log
.DS_Store
================================================
FILE: .prettierignore
================================================
node_modules
.next
.swc
coverage
dist
build
package.json
pnpm-lock.yaml
# Ignore example project built files if any
examples/app-router-showcase/.next
examples/app-router-showcase/out
================================================
FILE: .vscode/settings.json
================================================
{
"cSpell.words": ["noindex", "nofollow"],
"typescript.tsdk": "node_modules/typescript/lib"
}
================================================
FILE: ADDING_NEW_COMPONENTS.md
================================================
# Adding New Components to Next SEO
This 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.
## Table of Contents
1. [Research Phase](#1-research-phase)
2. [Type Definitions](#2-type-definitions)
3. [Component Implementation](#3-component-implementation)
4. [Export Configuration](#4-export-configuration)
5. [Unit Tests](#5-unit-tests)
6. [Documentation](#6-documentation)
7. [Example Pages](#7-example-pages)
8. [E2E Tests](#8-e2e-tests)
9. [Final Verification](#9-final-verification)
## 1. Research Phase
Before implementing, thoroughly research the structured data specification:
1. **Visit Google's Documentation**
- Go to [Google's Structured Data Gallery](https://developers.google.com/search/docs/appearance/structured-data/search-gallery)
- Find the specific type you're implementing (e.g., Article, Product, Recipe)
- Note all required and recommended properties
2. **Analyze Schema Types**
- Identify all subtypes (e.g., Article has NewsArticle, BlogPosting, Blog)
- Note property variations between types
- Check for special formatting requirements (dates, images, etc.)
3. **Review Existing Implementation**
- If updating from an older version, fetch the previous implementation
- Identify any missing features or properties
- Ensure backward compatibility where possible
## 2. Type Definitions
Create comprehensive TypeScript types in `src/types/[component].types.ts`:
```typescript
// src/types/article.types.ts
import type { ImageObject, Person, Organization, Author } from "./common.types";
// Note: Common types like ImageObject, Person, Organization, and Author
// are now defined in common.types.ts to avoid duplication
// Base interface with common properties
export interface ArticleBase {
headline: string;
url?: string;
author?: Author | Author[];
datePublished?: string;
dateModified?: string;
image?: string | ImageObject | (string | ImageObject)[];
publisher?: Organization;
description?: string;
isAccessibleForFree?: boolean;
mainEntityOfPage?:
| string
| {
"@type": "WebPage";
"@id": string;
};
}
// Specific schema type interfaces
export interface Article extends ArticleBase {
"@type": "Article";
}
export interface NewsArticle extends ArticleBase {
"@type": "NewsArticle";
}
export interface BlogPosting extends ArticleBase {
"@type": "BlogPosting";
}
// Component props type
export type ArticleJsonLdProps = (
| Omit
| Omit
| Omit
) & {
type?: "Article" | "NewsArticle" | "BlogPosting";
scriptId?: string;
scriptKey?: string;
};
```
### Key Patterns:
- Use union types for flexible inputs (e.g., `string | Person | Organization`)
- Support both single items and arrays where appropriate
- Extend common interfaces to reduce duplication
- Make all properties optional except truly required ones
- Include component-specific props like `scriptId` and `scriptKey`
- **Important**: Reuse types from `common.types.ts` for shared definitions like `ImageObject`, `Person`, `Organization`, and `Author`
### The @type Optional Pattern
A 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.
#### How It Works:
1. **Type Definitions**: Use `Omit` to create props that don't require `@type`:
```typescript
export type ArticleJsonLdProps = (
| Omit
| Omit
| Omit
) & {
type?: "Article" | "NewsArticle" | "BlogPosting";
// ... other props
};
```
2. **Process Functions**: Automatically add the correct `@type` based on input:
```typescript
// Developers can pass a simple string
author="John Doe"
// Process function converts it to a proper Person object
processAuthor("John Doe") // → { "@type": "Person", name: "John Doe" }
// Or pass an object without @type
author={{ name: "John Doe", url: "https://example.com" }}
// Process function adds @type intelligently
processAuthor({...}) // → { "@type": "Person", name: "John Doe", url: "..." }
```
3. **Intelligent Type Detection**: Process functions use property analysis to determine types:
- Objects with `logo`, `address`, or `contactPoint` → Organization
- Objects with `familyName` or `givenName` → Person
- Default fallbacks ensure valid Schema.org output
#### Benefits:
- **Less Boilerplate**: Developers don't need to remember Schema.org type names
- **Flexible Input**: Accept strings, objects with/without `@type`
- **Type Safety**: Full TypeScript support throughout
- **Forward Compatible**: Can still accept objects with `@type` if provided
## 3. Component Implementation
Create the component in `src/components/[Component]JsonLd.tsx`:
```typescript
// src/components/ArticleJsonLd.tsx
import { JsonLdScript } from "~/core/JsonLdScript";
import type { ArticleJsonLdProps } from "~/types/article.types";
import { processAuthor, processImage } from "~/utils/processors";
// Note: Common processing functions like processAuthor and processImage
// are now available in ~/utils/processors.ts to avoid duplication
export default function ArticleJsonLd({
type = "Article",
scriptId,
scriptKey,
headline,
url,
author,
datePublished,
dateModified,
image,
publisher,
description,
isAccessibleForFree,
mainEntityOfPage,
}: ArticleJsonLdProps) {
const data = {
"@context": "https://schema.org",
"@type": type,
headline,
...(url && { url }),
...(author && {
author: Array.isArray(author)
? author.map(processAuthor)
: processAuthor(author),
}),
...(datePublished && { datePublished }),
...(dateModified && { dateModified }),
// Apply defaults where appropriate
...(!dateModified && datePublished && { dateModified: datePublished }),
...(image && {
image: Array.isArray(image) ? image.map(processImage) : processImage(image),
}),
...(publisher && { publisher }),
...(description && { description }),
...(isAccessibleForFree !== undefined && { isAccessibleForFree }),
...(mainEntityOfPage && { mainEntityOfPage }),
};
return (
);
}
export type { ArticleJsonLdProps };
```
### Implementation Guidelines:
- Use the existing `JsonLdScript` component for rendering (now with TypeScript generics support)
- Process flexible inputs to proper schema format using shared utilities from `~/utils/processors`
- Use object spread with conditional inclusion for optional properties
- Handle arrays appropriately with `.map()`
- Apply sensible defaults (e.g., dateModified defaults to datePublished)
- Ensure boolean values are explicitly checked with `!== undefined`
- **Always use process functions** for properties that accept flexible types (strings, objects with/without `@type`)
- **Never require developers to specify `@type`** - the component should set the main `@type` from the `type` prop, and process functions should handle nested objects
## 4. Export Configuration
Update `src/index.ts` to export your component:
```typescript
export { JsonLdScript } from "./core/JsonLdScript";
export {
default as ArticleJsonLd,
type ArticleJsonLdProps,
} from "./components/ArticleJsonLd";
// Add your new component here
export const version = "7.0.0-alpha.0";
```
## 5. Unit Tests
Create comprehensive tests in `src/components/[Component]JsonLd.test.tsx`:
```typescript
import { render } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import ArticleJsonLd from "./ArticleJsonLd";
describe("ArticleJsonLd", () => {
it("renders basic Article with minimal props", () => {
const { container } = render(
);
const script = container.querySelector('script[type="application/ld+json"]');
expect(script).toBeTruthy();
const jsonData = JSON.parse(script!.textContent!);
expect(jsonData).toEqual({
"@context": "https://schema.org",
"@type": "Article",
headline: "Test Article",
datePublished: "2024-01-01T00:00:00.000Z",
dateModified: "2024-01-01T00:00:00.000Z", // defaults to datePublished
});
});
// Test each schema type
it("renders NewsArticle type when specified", () => {
// ... test implementation
});
// Test flexible inputs
it("handles string author", () => {
// ... converts string to Person object
});
it("handles multiple authors", () => {
// ... test array handling
});
// Test all properties
it("handles all optional properties", () => {
// ... comprehensive test with all props
});
// Test edge cases
it("handles isAccessibleForFree as false", () => {
// ... ensure boolean false is included
});
});
```
### Testing Checklist:
- ✅ Basic rendering with minimal props
- ✅ All schema type variations
- ✅ String to object conversions
- ✅ Array handling for authors and images
- ✅ All optional properties
- ✅ Default value application
- ✅ Boolean value handling
- ✅ Custom scriptId and scriptKey
## 6. Documentation
Add comprehensive documentation to `README.md`:
````markdown
### ArticleJsonLd
The `ArticleJsonLd` component helps you add structured data for articles, blog posts, and news articles to improve their appearance in search results.
#### Basic Usage
```tsx
import { ArticleJsonLd } from "next-seo";
;
```
````
#### Props
| Property | Type | Description |
| ---------- | --------------------------------------------- | ------------------------------------------ |
| `type` | `"Article" \| "NewsArticle" \| "BlogPosting"` | The type of article. Defaults to "Article" |
| `headline` | `string` | **Required.** The headline of the article |
| ... | ... | ... |
#### Best Practices
1. **Always include images**: Google recommends multiple aspect ratios
2. **Use ISO 8601 dates**: Include timezone information
3. **Multiple authors**: List all authors when applicable
````
## 7. Example Pages
Create example pages in `examples/app-router-showcase/app/[component]/page.tsx`:
```tsx
import { ArticleJsonLd } from "next-seo";
export default function ArticlePage() {
return (
Understanding Next.js App Router
{/* Article content */}
);
}
````
Create examples for:
- Basic usage (minimal props)
- Advanced usage (all features)
- Each schema type variation
## 8. E2E Tests
Create Playwright tests in `tests/e2e/[component]JsonLd.e2e.spec.ts`:
### Important E2E Testing Guidelines
**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.
❌ **DO NOT** use `page.route()` to inject mock HTML:
```typescript
// BAD - This is not a real E2E test!
await page.route("/test-page", async (route) => {
await route.fulfill({
body: `...`,
});
});
```
✅ **DO** create real example pages and test them:
```typescript
// GOOD - Test real pages with actual components
await page.goto("/article");
```
### Creating E2E Tests
For every E2E test scenario, you must:
1. Create a real example page in `examples/app-router-showcase/app/`
2. Write the E2E test to navigate to that page
3. Test the actual rendered output
```typescript
import { test, expect } from "@playwright/test";
test.describe("ArticleJsonLd", () => {
test("renders basic Article structured data", async ({ page }) => {
// Navigate to the real example page
await page.goto("/article");
const jsonLdScript = await page
.locator('script[type="application/ld+json"]')
.textContent();
expect(jsonLdScript).toBeTruthy();
const jsonData = JSON.parse(jsonLdScript!);
// Verify all properties
expect(jsonData["@context"]).toBe("https://schema.org");
expect(jsonData["@type"]).toBe("Article");
expect(jsonData.headline).toBe("Understanding Next.js App Router");
// ... test all properties
});
test("properly escapes HTML entities in content", async ({ page }) => {
// Navigate to a real example page with special characters
await page.goto("/article-special-chars");
const jsonLdScript = await page
.locator('script[type="application/ld+json"]')
.textContent();
// Verify JSON is valid and content is properly escaped
const jsonData = JSON.parse(jsonLdScript!);
expect(jsonData.headline).toContain("Special & Characters");
// Check that dangerous content is escaped in the raw JSON
expect(jsonLdScript).toContain("\\u003C/script>");
});
});
```
### When to Create Additional Example Pages
Create new example pages for:
- Basic usage with minimal props
- Advanced usage with all features
- Each schema type variation (e.g., Article, NewsArticle, BlogPosting)
- Special characters and HTML entities
- Edge cases with unusual data
- Different data combinations
Example structure:
```
examples/app-router-showcase/app/
├── article/ # Basic article example
├── article-advanced/ # All features
├── news-article/ # NewsArticle type
├── blog-posting/ # BlogPosting type
└── article-special-chars/ # Special characters test
```
You should also add a valid JSON test in `tests/e2e/jsonValidation.e2e.spec.ts`
### Security and Escaping Tests
**DO NOT add escape/security tests to individual component E2E tests!**
Security testing for escaping dangerous sequences (like ``, HTML comments, etc.) is handled centrally in `tests/e2e/security.e2e.spec.ts`. This test file comprehensively covers:
- Script tag injection prevention
- HTML comment escaping
- Edge cases with mixed dangerous patterns
- Safe rendering in Next.js-like environments
Individual component E2E tests should focus on:
- Component-specific functionality
- Correct data structure output
- Schema type variations
- Required and optional properties
The escaping functionality is a core library feature handled by the `stringify` utility, not something each component needs to test individually.
## 9. Final Verification
Before completing, run all quality checks:
```bash
# 1. Run unit tests
pnpm test:unit
# 2. Type checking
pnpm typecheck
# 3. Linting
pnpm lint
# 4. Build the package
pnpm build
```
Developer will run e2e manually as they can take a long time.
## Common Patterns and Best Practices
### Shared Utilities
The library now provides shared utilities to avoid code duplication:
1. **Common Types** (`~/types/common.types.ts`):
- `ImageObject`, `Person`, `Organization`, `Author`
- Base interfaces like `Thing`
2. **Processing Functions** (`~/utils/processors.ts`):
- `processAuthor(author: Author): Person | Organization`
- `processImage(image: string | ImageObject): string | ImageObject`
### Flexible Input Processing
Use the shared processing functions from `~/utils/processors`:
```typescript
import { processAuthor, processImage } from "~/utils/processors";
// These functions handle string-to-object conversions automatically
// and add the appropriate @type without developers needing to specify it
```
**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.
### Conditional Property Inclusion
Use object spread with conditional checks:
```typescript
const data = {
"@context": "https://schema.org",
"@type": type,
headline,
...(url && { url }), // Only include if truthy
...(isAccessibleForFree !== undefined && { isAccessibleForFree }), // Include false values
};
```
### Default Values
Apply sensible defaults where appropriate:
```typescript
// If dateModified is not provided but datePublished is, use datePublished
...(!dateModified && datePublished && { dateModified: datePublished }),
```
### Array Handling
Support both single items and arrays:
```typescript
...(author && {
author: Array.isArray(author)
? author.map(processAuthor)
: processAuthor(author),
}),
```
## Troubleshooting
### Common Issues
1. **ESLint errors about unused React import**
- Remove `import React from 'react'` - it's not needed with modern JSX transform
2. **Test failures with dateModified**
- Remember that dateModified defaults to datePublished when not provided
3. **Boolean properties not appearing**
- Use `!== undefined` check instead of truthy check for booleans
4. **Type errors with union types**
- Ensure proper type guards in processing functions
## Checklist for New Components
- [ ] Research Google's structured data documentation
- [ ] Create comprehensive type definitions (reuse common types from `common.types.ts`)
- [ ] Implement component using shared utilities from `~/utils/processors`
- [ ] Update exports in src/index.ts
- [ ] Write unit tests covering all scenarios
- [ ] Add documentation to README.md
- [ ] Create example pages for each variation
- [ ] Write E2E tests (Double check guidelines!)
- [ ] Run all quality checks (full sweep can be done via `pnpm test:sweep`)
- [ ] Ensure backward compatibility if updating existing component
- [ ] Check if any new processing functions should be added to shared utilities
================================================
FILE: AGENTS.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Next 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.
## Critical Rules
You must check these after coming up with a plan
[ ] Your plan adheres to the guide found in @ADDING_NEW_COMPONENTS.md
[ ] Your plan adheres to the guidelines found below
## Development Commands
### Installation
```bash
pnpm install
```
### Build & Development
```bash
pnpm dev # Watch mode with tsup
pnpm build # Build library code
```
### Code Quality
```bash
pnpm lint # Run ESLint
pnpm lint:fix # Fix ESLint issues
pnpm format # Format with Prettier
pnpm typecheck # Type checking with TypeScript
```
### Testing
```bash
pnpm test # Run typecheck + lint only
pnpm test:unit # Run unit tests with Vitest
pnpm test:unit:watch # Watch mode for unit tests
pnpm coverage # Generate coverage report
# Requires pnpm build to run first
pnpm test:e2e # Run E2E tests with Playwright
pnpm test:e2e:ui # Run E2E tests with UI
```
### Example App
```bash
pnpm example:dev # Run example app at localhost:3001
pnpm example:build # Build example app
pnpm example:start # Start example app
```
### Utilities
```bash
pnpm clean # Clean build artifacts
```
## Project Architecture
### Core Structure
- **`/src`** - Library source code
- **`/core`** - Core components like `JsonLdScript`
- **`/types`** - TypeScript type definitions
- **`/utils`** - Utility functions like `stringify`
- **`/examples/app-router-showcase`** - Example Next.js app for testing
- **`/tests`** - Test files
- **`/unit`** - Unit tests (Vitest)
- **`/e2e`** - E2E tests (Playwright)
### Build Configuration
- **tsup** - For building the library (see `tsup.config.ts`)
- Outputs both CommonJS and ESM formats
- Path alias: `~` maps to `./src`
### Testing Setup
- **Vitest** - Unit testing with React Testing Library
- **Playwright** - E2E testing running against example app on port 3001
- Tests use `~` alias for imports
## Development Notes
1. All library code is in `/src` directory
2. The project uses pnpm workspaces with the example app
3. When developing, the example app auto-starts on port 3001 for E2E tests
4. Lint and format are automatically run on staged files via Husky
5. The library exports both CommonJS and ESM formats with TypeScript definitions
6. When adding a new component ALWAYS refer to the guide found in ADDING_NEW_COMPONENTS.md
## Key Patterns
### @type Optional Pattern
Next SEO provides excellent developer experience by **never requiring developers to manually specify `@type` properties**. This is achieved through intelligent type definitions and process functions.
#### How It Works:
1. **Type Definitions**: Component props use `Omit` to make `@type` optional
2. **Process Functions**: Automatically add the correct `@type` based on the input
3. **Flexible Inputs**: Accept strings, objects with/without `@type`, and arrays
#### Example:
```typescript
// Developers can write this:
// Process functions transform it to valid Schema.org:
{
author: { "@type": "Person", name: "John Doe" },
publisher: { "@type": "Organization", name: "ACME Corp", logo: {...} }
}
```
#### Benefits:
- **Better DX**: No need to remember Schema.org type names
- **Less Boilerplate**: Cleaner, more readable code
- **Type Safety**: Full TypeScript support maintained
- **Flexibility**: Still accepts objects with `@type` if provided
#### Implementation Rules:
1. Always use process functions for properties accepting flexible types
2. Never require `@type` in component props
3. Use intelligent detection (e.g., `logo` property → Organization)
4. Provide sensible defaults in process functions
This pattern is fundamental to the library's design and must be maintained in all components.
================================================
FILE: CHANGELOG.md
================================================
# next-seo
## 7.2.0
### Minor Changes
- 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.
## 7.1.0
### Minor Changes
- d412e2b: Add HowToJsonLd component for structured data support
- New `HowToJsonLd` component following Schema.org HowTo specification
- Support for HowToStep, HowToSection, HowToDirection, and HowToTip types
- HowToSupply and HowToTool for materials and equipment
- Duration properties (prepTime, performTime, totalTime) in ISO 8601 format
- estimatedCost as string or MonetaryAmount object
- yield as string or QuantitativeValue
- Video support via VideoObject
## 7.0.1
### Patch Changes
- 1db3648: Add JSDoc comment to internal type guard function
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Next 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.
## Critical Rules
You must check these after coming up with a plan
[ ] Your plan adheres to the guide found in @ADDING_NEW_COMPONENTS.md
[ ] Your plan adheres to the guidelines found below
## Development Commands
### Installation
```bash
pnpm install
```
### Build & Development
```bash
pnpm dev # Watch mode with tsup
pnpm build # Build library code
```
### Code Quality
```bash
pnpm lint # Run ESLint
pnpm lint:fix # Fix ESLint issues
pnpm format # Format with Prettier
pnpm typecheck # Type checking with TypeScript
```
### Testing
```bash
pnpm test # Run typecheck + lint only
pnpm test:unit # Run unit tests with Vitest
pnpm test:unit:watch # Watch mode for unit tests
pnpm coverage # Generate coverage report
# Requires pnpm build to run first
pnpm test:e2e # Run E2E tests with Playwright
pnpm test:e2e:ui # Run E2E tests with UI
```
### Example App
```bash
pnpm example:dev # Run example app at localhost:3001
pnpm example:build # Build example app
pnpm example:start # Start example app
```
### Utilities
```bash
pnpm clean # Clean build artifacts
```
## Project Architecture
### Core Structure
- **`/src`** - Library source code
- **`/core`** - Core components like `JsonLdScript`
- **`/types`** - TypeScript type definitions
- **`/utils`** - Utility functions like `stringify`
- **`/examples/app-router-showcase`** - Example Next.js app for testing
- **`/tests`** - Test files
- **`/unit`** - Unit tests (Vitest)
- **`/e2e`** - E2E tests (Playwright)
### Build Configuration
- **tsup** - For building the library (see `tsup.config.ts`)
- Outputs both CommonJS and ESM formats
- Path alias: `~` maps to `./src`
### Testing Setup
- **Vitest** - Unit testing with React Testing Library
- **Playwright** - E2E testing running against example app on port 3001
- Tests use `~` alias for imports
## Development Notes
1. All library code is in `/src` directory
2. The project uses pnpm workspaces with the example app
3. When developing, the example app auto-starts on port 3001 for E2E tests
4. Lint and format are automatically run on staged files via Husky
5. The library exports both CommonJS and ESM formats with TypeScript definitions
6. When adding a new component ALWAYS refer to the guide found in ADDING_NEW_COMPONENTS.md
## Key Patterns
### @type Optional Pattern
Next SEO provides excellent developer experience by **never requiring developers to manually specify `@type` properties**. This is achieved through intelligent type definitions and process functions.
#### How It Works:
1. **Type Definitions**: Component props use `Omit` to make `@type` optional
2. **Process Functions**: Automatically add the correct `@type` based on the input
3. **Flexible Inputs**: Accept strings, objects with/without `@type`, and arrays
#### Example:
```typescript
// Developers can write this:
// Process functions transform it to valid Schema.org:
{
author: { "@type": "Person", name: "John Doe" },
publisher: { "@type": "Organization", name: "ACME Corp", logo: {...} }
}
```
#### Benefits:
- **Better DX**: No need to remember Schema.org type names
- **Less Boilerplate**: Cleaner, more readable code
- **Type Safety**: Full TypeScript support maintained
- **Flexibility**: Still accepts objects with `@type` if provided
#### Implementation Rules:
1. Always use process functions for properties accepting flexible types
2. Never require `@type` in component props
3. Use intelligent detection (e.g., `logo` property → Organization)
4. Provide sensible defaults in process functions
This pattern is fundamental to the library's design and must be maintained in all components.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Next SEO
Thank you for your interest in contributing to Next SEO! We are open to all and any contributions. This guide will help you get started.
It is critical that you look over the guidance for new components [here](ADDING_NEW_COMPONENTS.md)
## 🚀 Quick Start
1. Fork the repository
2. Clone your fork: `git clone git@github.com:your-username/next-seo.git`
3. Install dependencies: `pnpm install`
4. Create a new branch: `git checkout -b feature/your-feature-name`
5. Make your changes
6. Add a changeset: `pnpm changeset`
7. Submit a pull request
## 📦 Development Setup
### Prerequisites
- Node.js 20+ (LTS recommended)
- pnpm 9+ (`npm install -g pnpm`)
### Installation
```bash
# Clone the repository
git clone git@github.com:garmeeh/next-seo.git
cd next-seo
# Install dependencies
pnpm install
# Start development (watch mode)
pnpm dev
```
### Available Commands
```bash
pnpm dev # Watch mode development
pnpm build # Build the library
pnpm test # Run type checking and linting
pnpm test:unit # Run unit tests
pnpm test:e2e # Run E2E tests (requires build first)
pnpm test:sweep # Run full test suite (CI equivalent)
pnpm lint # Check linting
pnpm format # Format code with Prettier
```
## 📝 Adding a Changeset
**Important:** All PRs with code changes require a changeset. This helps us track changes and automatically manage releases.
### What is a changeset?
A changeset is a piece of information about changes made in a branch or commit. It includes:
- What packages changed
- What kind of change it was (major/minor/patch)
- A description of the change for the changelog
### How to add a changeset:
1. After making your changes, run: `pnpm changeset`
2. Select the packages affected (usually just `next-seo`)
3. Choose the type of change:
- **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**
- **minor**: New features, non-breaking enhancements (0.X.0) **Most common**
- **major**: Breaking changes (X.0.0)
4. Write a brief description of your changes
5. Commit the generated changeset file
### Example:
```bash
$ pnpm changeset
🦋 Which packages would you like to include? › next-seo
🦋 Which packages should have a major bump? › (none)
🦋 Which packages should have a minor bump? › next-seo
🦋 The following packages will be patch bumped:
🦋 next-seo@minor
🦋 Please enter a summary for this change:
📝 Added support for RecipeJsonLd component with full Schema.org compliance
```
We recommend never using patch unless critical security bug
This creates a markdown file in `.changeset/` that will be used to:
- Update the package version
- Generate changelog entries
- Credit you as a contributor
### When is a changeset NOT required?
- Documentation-only changes (README, etc.)
- Changes to GitHub workflows
- Changes to development tooling that don't affect the published package
## 🏗️ Project Guidelines
### For AI-Assisted Development
This project leverages AI coding tools. If you're using tools like Claude or GitHub Copilot:
- Refer to [CLAUDE.md](CLAUDE.md) for project-specific AI guidance
- Refer to [ADDING_NEW_COMPONENTS.md](ADDING_NEW_COMPONENTS.md) for component development
### For Large Features
If you're planning a large feature or refactor:
1. Open an issue first to discuss with maintainers
2. Provide comprehensive context in your issue/PR
3. Break down large changes into smaller, reviewable PRs if possible
## 🧪 Testing Requirements
Before submitting a PR, ensure all tests pass:
```bash
# Quick checks
pnpm test # Type checking and linting
pnpm test:unit # Unit tests
# Full validation (what CI runs)
pnpm test:sweep # Complete test suite
```
## 🔄 Pull Request Process
1. **Fork & Clone**: Fork the repo and clone locally
2. **Branch**: Create a feature branch from `main`
3. **Develop**: Make your changes following our guidelines
4. **Changeset**: Add a changeset describing your changes
5. **Test**: Ensure all tests pass
6. **Push**: Push to your fork
7. **PR**: Open a pull request with a clear description
### PR Guidelines
- Use clear, descriptive titles
- Reference any related issues
- Include examples if adding new features
- Ensure CI passes before requesting review
## ❓ Questions?
- Open a [Discussion](https://github.com/garmeeh/next-seo/discussions) for general questions
- Check existing issues and PRs
- Refer to the [README](./README.md) for usage documentation
## 📄 License
By contributing, you agree that your contributions will be licensed under the same MIT License that covers this project.
---
Thank you for contributing to Next SEO! Your efforts help make SEO easier for the Next.js community.
================================================
FILE: CUSTOM_COMPONENTS.md
================================================
# Creating Custom JSON-LD Components with Next SEO
This 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.
## Table of Contents
1. [Quick Start](#quick-start)
2. [Core Concepts](#core-concepts)
3. [Using Built-in Processors](#using-built-in-processors)
4. [Creating Custom Processors](#creating-custom-processors)
5. [Advanced Patterns](#advanced-patterns)
6. [Best Practices](#best-practices)
7. [Real-World Examples](#real-world-examples)
## Quick Start
Create a custom JSON-LD component in just a few lines:
```tsx
import { JsonLdScript, processors } from "next-seo";
export function PodcastEpisodeJsonLd({ name, author, duration, url }) {
const data = {
"@context": "https://schema.org",
"@type": "PodcastEpisode",
name,
...(url && { url }),
...(duration && { duration }),
...(author && { author: processors.processAuthor(author) }),
};
return ;
}
// Usage - no @type needed!
;
```
## Core Concepts
### The JsonLdScript Component
The `JsonLdScript` component is the foundation for rendering structured data:
```tsx
import { JsonLdScript } from "next-seo";
;
```
### The @type Optional Pattern
Next 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.
```tsx
// Your users write this:
author="John Doe"
// Your processor converts it to:
{ "@type": "Person", name: "John Doe" }
```
### Processors
Processors are functions that transform flexible inputs into properly typed Schema.org objects:
```tsx
import { processors } from "next-seo";
// Use built-in processors for common types
const author = processors.processAuthor("John Doe");
const image = processors.processImage({ url: "image.jpg", width: 800 });
const address = processors.processAddress("123 Main St");
```
## Using Built-in Processors
Next SEO provides 60+ processors for common Schema.org types:
### People & Organizations
```tsx
import { processors } from "next-seo";
// Flexible author input
processors.processAuthor("Jane Doe"); // → Person
processors.processAuthor({ name: "ACME Corp", logo: "..." }); // → Organization
// Other people/org processors
processors.processPublisher("Tech Publishing");
processors.processOrganizer({ name: "Event Co", url: "..." });
processors.processPerformer("Band Name");
```
### Media & Content
```tsx
// Images - string URL or ImageObject
processors.processImage("https://example.com/image.jpg");
processors.processImage({ url: "...", width: 800, height: 600 });
// Videos
processors.processVideo({
name: "Tutorial",
uploadDate: "2024-01-01",
thumbnailUrl: "...",
});
// Other media processors
processors.processLogo("logo.jpg");
processors.processScreenshot({ url: "...", caption: "App screenshot" });
```
### Locations & Places
```tsx
// Simple string becomes PostalAddress
processors.processAddress("123 Main St, City, Country");
// Object with more details
processors.processAddress({
streetAddress: "123 Main St",
addressLocality: "San Francisco",
addressRegion: "CA",
postalCode: "94105",
addressCountry: "US",
});
// Places with geo coordinates
processors.processPlace({
name: "Office",
geo: { latitude: 37.7749, longitude: -122.4194 },
});
```
### Commerce & Offers
```tsx
// Product offers
processors.processProductOffer({
price: 29.99,
priceCurrency: "USD",
availability: "https://schema.org/InStock",
});
// Return policies
processors.processMerchantReturnPolicy({
returnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow",
merchantReturnDays: 30,
});
```
## Creating Custom Processors
### Basic Custom Processor
Create processors for your specific needs:
```tsx
import { processors } from "next-seo";
// Custom processor for a podcast host
function processHost(host: string | { name: string; bio?: string }) {
if (typeof host === "string") {
return {
"@type": "Person",
name: host,
};
}
// Use the generic helper for objects
return processors.processSchemaType(host, "Person");
}
// Use in your component
export function PodcastJsonLd({ hosts, ...props }) {
const data = {
"@context": "https://schema.org",
"@type": "PodcastSeries",
...(hosts && {
host: Array.isArray(hosts) ? hosts.map(processHost) : processHost(hosts),
}),
};
return ;
}
```
### Advanced Custom Processor with Type Detection
Intelligently determine the type based on input properties:
```tsx
function processCreativeWork(work: string | Record) {
if (typeof work === "string") {
return {
"@type": "CreativeWork",
name: work,
};
}
// Already has @type? Return as-is
if (work["@type"]) {
return work;
}
// Detect type based on properties
let type = "CreativeWork";
if ("isbn" in work) type = "Book";
else if ("director" in work) type = "Movie";
else if ("artist" in work) type = "MusicRecording";
return {
"@type": type,
...work,
};
}
```
## Advanced Patterns
### Nested Processing
Process nested structures recursively:
```tsx
function processEventWithVenue(event: {
name: string;
venue?: string | { name: string; address?: string };
organizer?: string | { name: string };
}) {
return {
"@type": "Event",
name: event.name,
...(event.venue && {
location:
typeof event.venue === "string"
? processors.processPlace(event.venue)
: processors.processPlace({
...event.venue,
...(event.venue.address && {
address: processors.processAddress(event.venue.address),
}),
}),
}),
...(event.organizer && {
organizer: processors.processOrganizer(event.organizer),
}),
};
}
```
### Conditional Properties
Include properties only when they have values:
```tsx
export function CustomProductJsonLd({
name,
description,
price,
image,
brand,
reviews,
aggregateRating,
...props
}) {
const data = {
"@context": "https://schema.org",
"@type": "Product",
name,
...(description && { description }),
...(price && {
offers: {
"@type": "Offer",
price,
priceCurrency: "USD",
},
}),
...(image && {
image: Array.isArray(image)
? image.map(processors.processImage)
: processors.processImage(image),
}),
...(brand && { brand: processors.processBrand(brand) }),
...(reviews && {
review: Array.isArray(reviews)
? reviews.map(processors.processReview)
: processors.processReview(reviews),
}),
...(aggregateRating && {
aggregateRating: processors.processAggregateRating(aggregateRating),
}),
};
return ;
}
```
### Multiple Schema Types
Support different schema types with a type prop:
```tsx
type ScholarlyArticleType =
| "ScholarlyArticle"
| "MedicalScholarlyArticle"
| "TechArticle";
export function ScholarlyArticleJsonLd({
type = "ScholarlyArticle",
headline,
author,
datePublished,
journal,
doi,
...props
}: {
type?: ScholarlyArticleType;
headline: string;
author: string | Array;
datePublished: string;
journal?: string;
doi?: string;
}) {
const data = {
"@context": "https://schema.org",
"@type": type,
headline,
datePublished,
...(author && {
author: Array.isArray(author)
? author.map(processors.processAuthor)
: processors.processAuthor(author),
}),
...(journal && {
isPartOf: {
"@type": "PublicationIssue",
name: journal,
},
}),
...(doi && { identifier: processors.processIdentifier(doi) }),
};
return (
);
}
```
## Best Practices
### 1. Always Use Processors for Flexible Types
```tsx
// ✅ Good - uses processor
author: processors.processAuthor(author)
// ❌ Bad - requires user to specify @type
author: { "@type": "Person", ...author }
```
### 2. Handle Arrays and Single Values
```tsx
// Support both single and array inputs
...(tags && {
keywords: Array.isArray(tags) ? tags.join(', ') : tags
})
```
### 3. Apply Sensible Defaults
```tsx
// Default dateModified to datePublished if not provided
const data = {
datePublished,
dateModified: dateModified || datePublished,
};
```
### 4. Use TypeScript for Better DX
```tsx
interface ServiceJsonLdProps {
name: string;
provider?: string | Organization;
areaServed?: string | string[];
serviceType?: string;
scriptId?: string;
scriptKey?: string;
}
```
### 5. Document Your Component
```tsx
/**
* ServiceJsonLd - Structured data for service offerings
*
* @example
*
*/
export function ServiceJsonLd({ ... }) { ... }
```
## Real-World Examples
### 1. Podcast Series with Episodes
```tsx
import { JsonLdScript, processors } from "next-seo";
interface PodcastSeriesProps {
name: string;
description?: string;
host?: string | Array;
episodes?: Array<{
name: string;
url?: string;
duration?: string;
datePublished?: string;
}>;
image?: string | { url: string; width?: number; height?: number };
scriptKey?: string;
}
export function PodcastSeriesJsonLd({
name,
description,
host,
episodes,
image,
scriptKey = "podcast-series",
}: PodcastSeriesProps) {
const data = {
"@context": "https://schema.org",
"@type": "PodcastSeries",
name,
...(description && { description }),
...(host && {
host: Array.isArray(host)
? host.map((h) =>
typeof h === "string"
? { "@type": "Person", name: h }
: processors.processAuthor(h),
)
: typeof host === "string"
? { "@type": "Person", name: host }
: processors.processAuthor(host),
}),
...(image && { image: processors.processImage(image) }),
...(episodes && {
episode: episodes.map((ep, index) => ({
"@type": "PodcastEpisode",
name: ep.name,
position: index + 1,
...(ep.url && { url: ep.url }),
...(ep.duration && { duration: ep.duration }),
...(ep.datePublished && { datePublished: ep.datePublished }),
})),
}),
};
return ;
}
```
### 2. Real Estate Listing
```tsx
import { JsonLdScript, processors, type ImageObject } from "next-seo";
interface RealEstateListingProps {
name: string;
description?: string;
price: number;
priceCurrency?: string;
address: string | Record;
images?: Array;
numberOfRooms?: number;
floorSize?: { value: number; unitCode: string };
yearBuilt?: number;
scriptKey?: string;
}
export function RealEstateListingJsonLd({
name,
description,
price,
priceCurrency = "USD",
address,
images,
numberOfRooms,
floorSize,
yearBuilt,
scriptKey = "real-estate",
}: RealEstateListingProps) {
const data = {
"@context": "https://schema.org",
"@type": "RealEstateListing",
name,
...(description && { description }),
offers: {
"@type": "Offer",
price,
priceCurrency,
},
address: processors.processAddress(address),
...(images && {
image: images.map(processors.processImage),
}),
...(numberOfRooms && { numberOfRooms }),
...(floorSize && {
floorSize: processors.processQuantitativeValue(floorSize),
}),
...(yearBuilt && { yearBuilt }),
};
return ;
}
```
### 3. Service with Pricing Tiers
```tsx
import { JsonLdScript, processors } from "next-seo";
interface ServiceWithPricingProps {
name: string;
provider: string | { name: string; url?: string };
description?: string;
pricingTiers?: Array<{
name: string;
price: number | { min: number; max: number };
features?: string[];
}>;
areaServed?: string | string[];
scriptKey?: string;
}
export function ServiceWithPricingJsonLd({
name,
provider,
description,
pricingTiers,
areaServed,
scriptKey = "service",
}: ServiceWithPricingProps) {
const data = {
"@context": "https://schema.org",
"@type": "Service",
name,
provider: processors.processOrganization(provider),
...(description && { description }),
...(pricingTiers && {
hasOfferCatalog: {
"@type": "OfferCatalog",
name: `${name} Pricing`,
itemListElement: pricingTiers.map((tier) => ({
"@type": "Offer",
name: tier.name,
...(typeof tier.price === "number"
? { price: tier.price }
: {
priceSpecification: {
"@type": "PriceSpecification",
minPrice: tier.price.min,
maxPrice: tier.price.max,
priceCurrency: "USD",
},
}),
...(tier.features && {
description: tier.features.join(", "),
}),
})),
},
}),
...(areaServed && {
areaServed: Array.isArray(areaServed) ? areaServed : [areaServed],
}),
};
return ;
}
```
### 4. Educational Course with Modules
```tsx
import { JsonLdScript, processors } from "next-seo";
interface CourseWithModulesProps {
name: string;
description: string;
provider: string | { name: string; url?: string };
instructor?: string | Array;
modules?: Array<{
name: string;
description?: string;
duration?: string;
}>;
price?: number;
startDate?: string;
endDate?: string;
scriptKey?: string;
}
export function CourseWithModulesJsonLd({
name,
description,
provider,
instructor,
modules,
price,
startDate,
endDate,
scriptKey = "course",
}: CourseWithModulesProps) {
const data = {
"@context": "https://schema.org",
"@type": "Course",
name,
description,
provider: processors.processProvider(provider),
...(instructor && {
instructor: Array.isArray(instructor)
? instructor.map(processors.processAuthor)
: processors.processAuthor(instructor),
}),
...(modules && {
hasCourseInstance: modules.map((module, index) => ({
"@type": "CourseInstance",
name: module.name,
courseMode: "online",
position: index + 1,
...(module.description && { description: module.description }),
...(module.duration && { duration: module.duration }),
})),
}),
...(price !== undefined && {
offers: {
"@type": "Offer",
price,
priceCurrency: "USD",
...(startDate && { validFrom: startDate }),
...(endDate && { validThrough: endDate }),
},
}),
};
return ;
}
```
## Testing Your Components
### Unit Testing
```tsx
import { render } from "@testing-library/react";
import { ServiceJsonLd } from "./ServiceJsonLd";
describe("ServiceJsonLd", () => {
it("renders service with basic props", () => {
const { container } = render(
,
);
const script = container.querySelector(
'script[type="application/ld+json"]',
);
const data = JSON.parse(script.textContent);
expect(data["@type"]).toBe("Service");
expect(data.name).toBe("Consulting Service");
expect(data.provider["@type"]).toBe("Organization");
});
});
```
### Validation
Use Google's Rich Results Test to validate your structured data:
1. Deploy your page with the custom component
2. Visit [Google Rich Results Test](https://search.google.com/test/rich-results)
3. Enter your URL and check for errors
## Migration Guide
If you're migrating from inline JSON-LD to next-seo custom components:
### Before (Inline JSON-LD)
```tsx
```
### After (Custom Component)
```tsx
import { JsonLdScript, processors } from "next-seo";
export function ServiceJsonLd({ name, provider }) {
const data = {
"@context": "https://schema.org",
"@type": "Service",
name,
provider: processors.processOrganization(provider),
};
return ;
}
// Usage - cleaner and type-safe!
;
```
## Processor API Reference
For a complete list of available processors, see the [processors export file](./src/utils/processors.export.ts). Key processors include:
- `processSchemaType(value, type)` - Generic processor for any schema type
- `processAuthor(author)` - Person or Organization
- `processImage(image)` - String URL or ImageObject
- `processAddress(address)` - String or PostalAddress
- `processPlace(place)` - String or Place with address
- `processOffer(offer)` - Offer with price and availability
- `processReview(review)` - Review with rating and author
- `processAggregateRating(rating)` - Aggregate rating with count
- And 50+ more specialized processors...
## Getting Help
- Check existing components in [src/components](./src/components) for patterns
- Review [ADDING_NEW_COMPONENTS.md](./ADDING_NEW_COMPONENTS.md) for internal component development
- Open an issue for processor requests or questions
- Contribute new processors via PR
## Summary
Creating custom JSON-LD components with next-seo is simple:
1. Import `JsonLdScript` and `processors`
2. Define your component props (TypeScript recommended)
3. Use processors for flexible input handling
4. Apply the @type optional pattern
5. Return JsonLdScript with your data
This approach gives you:
- ✅ Type safety with TypeScript
- ✅ Flexible input handling
- ✅ No @type boilerplate for users
- ✅ Consistent with next-seo patterns
- ✅ Easy to test and maintain
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Gary Meehan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) 2018 Gary Meehan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: LIST.md
================================================
# Google Search Supported Structured Data Components
This list tracks which Google Search supported structured data types are implemented in next-seo.
## Implementation Status
- [x] Article
- [~] Book actions (Upcoming Deprecation)
- [x] Breadcrumb
- [x] Carousel
- [~] Course info (Upcoming Deprecation)
- [x] Course list
- [x] Dataset
- [x] Discussion forum
- [x] Education Q&A (implemented as QuizJsonLd)
- [x] Employer aggregate rating
- [~] Estimated salary (Upcoming Deprecation)
- [x] Event
- [x] Fact check
- [x] FAQ
- [x] Image metadata
- [x] Job posting
- [~] Learning video (Upcoming Deprecation)
- [x] Local business
- [ ] Math solver
- [x] Movie carousel
- [x] Organization
- [ ] Practice problem
- [x] Product
- [x] Merchant Listing
- [x] Variants
- [x] Loyalty Program
- [x] Merchant Return Policy
- [x] Profile page
- [x] Q&A
- [x] Recipe
- [x] Review snippet
- [x] Software app
- [ ] Speakable
- [~] Special announcement (Upcoming Deprecation)
- [x] Subscription and paywalled content
- [x] Vacation rental
- [~] Vehicle listing (Upcoming Deprecation)
- [x] Video
## Notes
- Education Q&A is implemented as `QuizJsonLd` in next-seo
- Some components marked with icons in the Google documentation may have special requirements or be in beta
================================================
FILE: README.md
================================================
**Outrank**
Get 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)
[](https://outrank.so/?via=next-seo)
**Have you seen the new Next.js newsletter?**
[
](https://dub.sh/nextjsweekly)
# Next SEO




Next 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.
## 📋 Table of Contents
_Looking for v6 documentation? [View Here](https://github.com/garmeeh/next-seo/tree/master)_
_Still using component in Pages? View docs here [/src/pages/README.md]_
## 🚀 Quick Start
### Installation
```bash
npm install next-seo
# or
yarn add next-seo
# or
pnpm add next-seo
# or
bun add next-seo
```
### Basic Usage
```tsx
import { ArticleJsonLd } from "next-seo";
export default function BlogPost() {
return (
<>
Getting Started with Next SEO
{/* Your content */}
>
);
}
```
> **Note**: For standard meta tags (``, ``), use Next.js's built-in [`generateMetadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) function.
> **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.
## Support This Project
**Feel like supporting this free plugin?**
It takes a lot of time to maintain an open source project so any small contribution is greatly appreciated.
Coffee fuels coding ☕️
## Components
### ArticleJsonLd
The `ArticleJsonLd` component helps you add structured data for articles, blog posts, and news articles to improve their appearance in search results.
#### Basic Usage
```tsx
import { ArticleJsonLd } from "next-seo";
export default function ArticlePage() {
return (
<>
My Amazing Article
{/* Article content */}
>
);
}
```
#### Advanced Example with Multiple Authors
```tsx
```
#### Blog Posting Example
```tsx
```
#### Props
| Property | Type | Description |
| --------------------- | ------------------------------------------------------- | -------------------------------------------------------- |
| `type` | `"Article" \| "NewsArticle" \| "BlogPosting" \| "Blog"` | The type of article. Defaults to "Article" |
| `headline` | `string` | **Required.** The headline of the article |
| `url` | `string` | The canonical URL of the article |
| `author` | `string \| Person \| Organization \| Author[]` | The author(s) of the article |
| `datePublished` | `string` | ISO 8601 date when the article was published |
| `dateModified` | `string` | ISO 8601 date when the article was last modified |
| `image` | `string \| ImageObject \| (string \| ImageObject)[]` | Article images. Google recommends multiple aspect ratios |
| `publisher` | `Organization` | The publisher of the article |
| `description` | `string` | A short description of the article |
| `isAccessibleForFree` | `boolean` | Whether the article is accessible for free |
| `mainEntityOfPage` | `string \| WebPage` | Indicates the article is the primary content of the page |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key prop for React |
#### Best Practices
1. **Always include images**: Google strongly recommends including high-resolution images with multiple aspect ratios (16x9, 4x3, 1x1)
2. **Use ISO 8601 dates**: Include timezone information for accuracy
3. **Multiple authors**: List all authors when applicable
4. **Publisher logo**: Include a logo for NewsArticle type
5. **Update dateModified**: Keep this current when updating content
[↑ Back to Components](#-components-by-category)
### ClaimReviewJsonLd
The `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.
#### Basic Usage
```tsx
import { ClaimReviewJsonLd } from "next-seo";
export default function FactCheckPage() {
return (
<>
Fact Check: The World is Flat
{/* Your fact check content */}
>
);
}
```
#### Props
| Property | Type | Description |
| --------------- | ---------------------------------- | ------------------------------------------------------------------------------------- |
| `claimReviewed` | `string` | **Required.** A short summary of the claim being evaluated (keep under 75 characters) |
| `reviewRating` | `object` | **Required.** The assessment of the claim with rating value and textual rating |
| `url` | `string` | **Required.** Link to the page hosting the full fact check article |
| `author` | `string \| Organization \| Person` | The publisher of the fact check article |
| `itemReviewed` | `Claim` | Detailed information about the claim being reviewed |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key for script identification |
#### Review Rating Properties
| Property | Type | Description |
| --------------- | -------- | ------------------------------------------------------------------------------------------- |
| `alternateName` | `string` | **Required.** The truthfulness rating as human-readable text (e.g., "False", "Mostly true") |
| `ratingValue` | `number` | **Required.** Numeric rating (closer to bestRating = more true) |
| `bestRating` | `number` | Best value in the rating scale (must be greater than worstRating) |
| `worstRating` | `number` | Worst value in the rating scale (minimum value of 1) |
| `name` | `string` | Alternative to alternateName (use alternateName instead) |
#### Advanced Example with Claim Details
```tsx
```
#### Best Practices
1. **Clear ratings**: Use descriptive alternateName values that clearly indicate the verdict
2. **Claim summary**: Keep claimReviewed concise (under 75 characters) to prevent wrapping
3. **Full context**: Include itemReviewed when possible to provide claim origin details
4. **Consistent scale**: Use a consistent rating scale across all your fact checks
5. **Author credibility**: Clearly identify your fact-checking organization
[↑ Back to Components](#-components-by-category)
### CreativeWorkJsonLd
The `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.
#### Basic Usage
```tsx
import { CreativeWorkJsonLd } from "next-seo";
export default function ArticlePage() {
return (
<>
Premium Article
Free preview content here...
Premium content that requires subscription...
>
);
}
```
#### Props
| Property | Type | Description |
| --------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `type` | `"CreativeWork" \| "Article" \| "NewsArticle" \| "Blog" \| "BlogPosting" \| "Comment" \| ...` | The type of creative work. Defaults to "CreativeWork" |
| `headline` | `string` | The headline of the content (used for Article types) |
| `name` | `string` | The name of the content (alternative to headline) |
| `url` | `string` | URL of the content |
| `author` | `string \| Person \| Organization \| Array` | Author(s) of the content |
| `datePublished` | `string` | ISO 8601 publication date |
| `dateModified` | `string` | ISO 8601 modification date |
| `image` | `string \| ImageObject \| Array` | Image(s) associated with the content |
| `publisher` | `string \| Organization \| Person` | Publisher of the content |
| `description` | `string` | Description of the content |
| `isAccessibleForFree` | `boolean` | Whether the content is free or requires payment/subscription |
| `hasPart` | `WebPageElement \| WebPageElement[]` | Marks specific sections as paywalled |
| `mainEntityOfPage` | `string \| WebPage` | The main page for this content |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key for script identification |
#### WebPageElement Properties (for hasPart)
| Property | Type | Description |
| --------------------- | --------- | --------------------------------------------------- |
| `isAccessibleForFree` | `boolean` | **Required.** Whether this section is free (false) |
| `cssSelector` | `string` | **Required.** CSS class selector (e.g., ".paywall") |
#### Marking Paywalled Content
```tsx
```
#### Multiple Paywalled Sections
```tsx
```
#### Different CreativeWork Types
```tsx
// Blog with subscription content
// Comment
// Course with provider
// Review with rating
```
#### Best Practices
1. **Use specific types**: Choose the most specific CreativeWork type (Article, NewsArticle, etc.) when applicable
2. **Mark paywalled sections**: Use `hasPart` with `cssSelector` to identify paywalled content sections
3. **Class selectors only**: Only use class selectors (e.g., ".paywall") for `cssSelector`, not IDs or other selectors
4. **Consistent selectors**: Ensure your HTML classes match the `cssSelector` values exactly
5. **Complete metadata**: Include as much metadata as possible (author, dates, images) for better search results
[↑ Back to Components](#-components-by-category)
### RecipeJsonLd
The `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.
#### Basic Usage
```tsx
import { RecipeJsonLd } from "next-seo";
export default function RecipePage() {
return (
<>
Simple Chocolate Chip Cookies
{/* Recipe content */}
>
);
}
```
#### Advanced Example with Structured Instructions and Nutrition
```tsx
```
#### Props
| Property | Type | Description |
| -------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `name` | `string` | **Required.** The name of the dish |
| `image` | `string \| ImageObject \| (string \| ImageObject)[]` | **Required.** Images of the completed dish. Google recommends multiple high-resolution images |
| `description` | `string` | A short summary describing the dish |
| `author` | `string \| Person \| Organization` | The creator of the recipe |
| `datePublished` | `string` | ISO 8601 date when the recipe was published |
| `url` | `string` | The canonical URL of the recipe page |
| `prepTime` | `string` | ISO 8601 duration for preparation time (e.g., "PT30M" for 30 minutes) |
| `cookTime` | `string` | ISO 8601 duration for cooking time |
| `totalTime` | `string` | ISO 8601 duration for total time (prep + cook) |
| `recipeYield` | `string \| number` | The quantity produced (e.g., "4 servings", "1 loaf", or just 6) |
| `recipeCategory` | `string` | The type of meal or course (e.g., "dessert", "main course") |
| `recipeCuisine` | `string` | The cuisine of the recipe (e.g., "French", "Mexican") |
| `recipeIngredient` | `string[]` | List of ingredients with quantities |
| `recipeInstructions` | `string \| HowToStep \| HowToSection \| (string \| HowToStep \| HowToSection)[]` | Step-by-step instructions |
| `nutrition` | `NutritionInformation` | Nutritional information per serving |
| `aggregateRating` | `AggregateRating` | The aggregate rating from users |
| `video` | `VideoObject` | A video showing how to make the recipe |
| `keywords` | `string` | Keywords about the recipe, separated by commas |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key prop for React |
#### Duration Format (ISO 8601)
Use these formats for time durations:
- `PT15M` - 15 minutes
- `PT1H` - 1 hour
- `PT1H30M` - 1 hour 30 minutes
- `PT2H15M` - 2 hours 15 minutes
#### Best Practices
1. **High-quality images**: Include multiple high-resolution images (16x9, 4x3, 1x1 aspect ratios)
2. **Detailed instructions**: Use HowToStep objects for better structured data
3. **Complete nutrition info**: Include nutrition data when possible for better search visibility
4. **Accurate times**: Always provide prepTime and cookTime together
5. **Ratings**: Include aggregateRating when you have user reviews
6. **Video content**: Adding a video significantly improves search appearance
[↑ Back to Components](#-components-by-category)
### HowToJsonLd
The `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.
#### Basic Usage
```tsx
import { HowToJsonLd } from "next-seo";
export default function HowToPage() {
return (
<>
How to Change a Flat Tire
{/* Guide content */}
>
);
}
```
#### Advanced Example with Sections and Detailed Steps
```tsx
```
#### Props
| Property | Type | Description |
| --------------- | ---------------------------------------------------- | ------------------------------------------------------------------------- |
| `name` | `string` | **Required.** The title of the how-to guide |
| `description` | `string` | A brief description of the guide |
| `image` | `string \| ImageObject` | An image of the completed task or project |
| `estimatedCost` | `string \| MonetaryAmount` | The estimated cost of supplies (e.g., "$20" or MonetaryAmount object) |
| `prepTime` | `string` | ISO 8601 duration for preparation time |
| `performTime` | `string` | ISO 8601 duration for the time to perform the instructions |
| `totalTime` | `string` | ISO 8601 duration for total time (prep + perform) |
| `yield` | `string \| QuantitativeValue` | The result of performing the instructions (e.g., "1 birdhouse") |
| `supply` | `string \| HowToSupply \| (string \| HowToSupply)[]` | Supplies consumed when performing the task |
| `tool` | `string \| HowToTool \| (string \| HowToTool)[]` | Tools used but not consumed |
| `step` | `string \| HowToStep \| HowToSection \| (Step)[]` | The steps to complete the task. Can be simple strings, steps, or sections |
| `video` | `VideoObject` | A video showing how to complete the task |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key prop for React |
#### Step Types
**HowToStep** - A single step in the guide:
```tsx
{
"@type": "HowToStep",
name: "Step Name", // Optional step title
text: "Step instructions", // The instruction text
url: "https://...", // Optional URL for more details
image: "https://...", // Optional step image
}
```
**HowToSection** - A group of related steps:
```tsx
{
"@type": "HowToSection",
name: "Section Name",
position: 1,
itemListElement: [
{ "@type": "HowToStep", text: "First step" },
{ "@type": "HowToStep", text: "Second step" },
]
}
```
**HowToDirection** and **HowToTip** - For detailed step content:
```tsx
{
"@type": "HowToStep",
itemListElement: [
{
"@type": "HowToDirection",
text: "Do this specific action",
beforeMedia: "https://example.com/before.jpg",
afterMedia: "https://example.com/after.jpg",
},
{
"@type": "HowToTip",
text: "Here's a helpful tip",
}
]
}
```
#### Duration Format (ISO 8601)
Use these formats for time durations:
- `PT15M` - 15 minutes
- `PT1H` - 1 hour
- `PT1H30M` - 1 hour 30 minutes
- `PT2H15M` - 2 hours 15 minutes
#### Best Practices
1. **Clear steps**: Write concise, actionable step instructions
2. **Include images**: Add images for complex steps to improve clarity
3. **Separate sections**: Use HowToSection to group related steps logically
4. **Accurate timing**: Provide realistic time estimates for each phase
5. **List all materials**: Include all supplies and tools needed upfront
6. **Add video**: Video content significantly improves search appearance
[↑ Back to Components](#-components-by-category)
### OrganizationJsonLd
The `OrganizationJsonLd` component helps you add structured data about your organization to improve how it appears in search results and knowledge panels.
#### Basic Usage
```tsx
import { OrganizationJsonLd } from "next-seo";
export default function AboutPage() {
return (
<>
About Example Corporation
{/* About page content */}
>
);
}
```
#### Advanced Example with Address and Contact
```tsx
```
#### OnlineStore Example with Return Policy
```tsx
```
#### Props
| Property | Type | Description |
| ------------------------- | -------------------------------------------------------- | ------------------------------------------------------- |
| `type` | `"Organization" \| "OnlineStore"` | The type of organization. Defaults to "Organization" |
| `name` | `string` | The name of your organization |
| `url` | `string` | The URL of your organization's website |
| `logo` | `string \| ImageObject` | Your organization's logo (112x112px minimum) |
| `description` | `string` | A detailed description of your organization |
| `sameAs` | `string \| string[]` | URLs of your organization's profiles on other sites |
| `address` | `string \| PostalAddress \| (string \| PostalAddress)[]` | Physical or mailing address(es) |
| `contactPoint` | `ContactPoint \| ContactPoint[]` | Contact information for your organization |
| `telephone` | `string` | Primary phone number (include country code) |
| `email` | `string` | Primary email address |
| `alternateName` | `string` | Alternative name your organization goes by |
| `foundingDate` | `string` | ISO 8601 date when the organization was founded |
| `legalName` | `string` | Registered legal name if different from name |
| `taxID` | `string` | Tax ID associated with your organization |
| `vatID` | `string` | VAT code (important trust signal) |
| `duns` | `string` | Dun & Bradstreet DUNS number |
| `leiCode` | `string` | Legal Entity Identifier (ISO 17442) |
| `naics` | `string` | North American Industry Classification System code |
| `globalLocationNumber` | `string` | GS1 Global Location Number |
| `iso6523Code` | `string` | ISO 6523 identifier (e.g., "0199:724500PMK2A2M1SQQ228") |
| `numberOfEmployees` | `number \| QuantitativeValue` | Number of employees or range |
| `review` | `Review \| Review[]` | A review or array of reviews of the organization |
| `aggregateRating` | `AggregateRating` | The overall rating based on a collection of reviews |
| `hasMerchantReturnPolicy` | `MerchantReturnPolicy \| MerchantReturnPolicy[]` | Return policy details (OnlineStore only) |
| `hasMemberProgram` | `MemberProgram \| MemberProgram[]` | Loyalty/membership program details (OnlineStore only) |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key prop for React |
#### Organization with Reviews and Ratings
```tsx
```
#### OnlineStore with Loyalty Program Example
```tsx
```
#### Multiple Loyalty Programs Example
```tsx
```
#### MemberProgram Properties
| Property | Type | Description |
| ------------- | ------------------------------------------ | --------------------------------------------- |
| `name` | `string` | **Required**. Name of the loyalty program |
| `description` | `string` | **Required**. Description of program benefits |
| `url` | `string` | URL where customers can sign up |
| `hasTiers` | `MemberProgramTier \| MemberProgramTier[]` | **Required**. Tier(s) of the loyalty program |
#### MemberProgramTier Properties
| Property | Type | Description |
| ------------------------ | ----------------------------- | ------------------------------------ |
| `name` | `string` | **Required**. Name of the tier |
| `hasTierBenefit` | `string \| string[]` | **Required**. Benefits for this tier |
| `hasTierRequirement` | `various` (see below) | Requirements to join this tier |
| `membershipPointsEarned` | `number \| QuantitativeValue` | Points earned per unit spent |
| `url` | `string` | URL for tier-specific signup |
| `@id` | `string` | Unique identifier for the tier |
#### Tier Benefits
Benefits can be specified using short names or full URLs:
- `"TierBenefitLoyaltyPoints"` or `"https://schema.org/TierBenefitLoyaltyPoints"` - Earn loyalty points
- `"TierBenefitLoyaltyPrice"` or `"https://schema.org/TierBenefitLoyaltyPrice"` - Special member pricing
#### Tier Requirements
The `hasTierRequirement` property accepts different types based on the requirement:
**Credit Card Requirement:**
```tsx
hasTierRequirement: {
name: "Store Premium Credit Card";
}
```
**Minimum Spending Requirement (MonetaryAmount):**
```tsx
hasTierRequirement: {
value: 1000,
currency: "USD"
}
```
**Subscription Fee (UnitPriceSpecification):**
```tsx
hasTierRequirement: {
price: 9.99,
priceCurrency: "EUR",
billingDuration: 12, // Total duration
billingIncrement: 1, // Billing frequency
unitCode: "MON" // Unit (MON = monthly)
}
```
**Text Description:**
```tsx
hasTierRequirement: "By invitation only - must maintain $10,000+ annual spending";
```
#### Membership Points Earned
Points can be specified as a simple number or as a detailed QuantitativeValue:
**Simple:**
```tsx
membershipPointsEarned: 5;
```
**Detailed:**
```tsx
membershipPointsEarned: {
value: 10,
minValue: 10,
maxValue: 20,
unitText: "points per dollar (double on special events)"
}
```
#### Best Practices
1. **Place on homepage or about page**: Add this markup to your homepage or a dedicated "about us" page
2. **Use specific subtypes**: Use "OnlineStore" for e-commerce sites rather than generic "Organization"
3. **Include identifiers**: Add VAT ID, ISO codes, and other identifiers for better trust signals
4. **Complete address information**: Provide full address details including country code
5. **Multiple locations**: Use array format for addresses if you have multiple locations
6. **High-quality logo**: Use a logo that looks good on white background, minimum 112x112px
[↑ Back to Components](#-components-by-category)
### LocalBusinessJsonLd
The `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.
#### Basic Usage
```tsx
import { LocalBusinessJsonLd } from "next-seo";
;
```
#### Restaurant Example with Full Details
```tsx
```
#### Store with Departments
```tsx
```
#### Props
| Property | Type | Description |
| --------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------- |
| `type` | `string \| string[]` | Business type (e.g., "Restaurant", "Store", or ["Restaurant", "BarOrPub"]) |
| `name` | `string` | **Required.** The name of the business |
| `address` | `string \| PostalAddress \| (string \| PostalAddress)[]` | **Required.** Physical location(s) of the business |
| `url` | `string` | The fully-qualified URL of the business location page |
| `telephone` | `string` | Primary contact phone number (include country code) |
| `image` | `string \| ImageObject \| (string \| ImageObject)[]` | Images of the business (multiple aspect ratios recommended) |
| `priceRange` | `string` | Relative price range (e.g., "$", "$$", "$$$", or "$10-15") |
| `geo` | `GeoCoordinates` | Geographic coordinates (min 5 decimal places precision) |
| `openingHoursSpecification` | `OpeningHoursSpecification \| OpeningHoursSpecification[]` | Business hours including special/seasonal hours |
| `review` | `Review \| Review[]` | Customer reviews (for review sites only) |
| `aggregateRating` | `AggregateRating` | Average rating based on multiple reviews |
| `department` | `LocalBusinessBase \| LocalBusinessBase[]` | Departments within the business |
| `menu` | `string` | URL of the menu (for food establishments) |
| `servesCuisine` | `string \| string[]` | Type of cuisine served (for restaurants) |
| `sameAs` | `string \| string[]` | URLs of business profiles on other sites |
| `branchOf` | `Organization` | Parent organization if this is a branch |
| `currenciesAccepted` | `string` | Currencies accepted (e.g., "USD") |
| `paymentAccepted` | `string` | Payment methods accepted |
| `areaServed` | `string \| string[]` | Geographic areas served |
| `email` | `string` | Business email address |
| `faxNumber` | `string` | Business fax number |
| `slogan` | `string` | Business slogan or tagline |
| `description` | `string` | Detailed description of the business |
| `publicAccess` | `boolean` | Whether the business location is accessible to the public |
| `smokingAllowed` | `boolean` | Whether smoking is allowed at the location |
| `isAccessibleForFree` | `boolean` | Whether access is free |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key prop for React |
#### Opening Hours Examples
**Standard Business Hours:**
```tsx
openingHoursSpecification={[
{
"@type": "OpeningHoursSpecification",
dayOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
opens: "09:00",
closes: "17:00",
},
{
"@type": "OpeningHoursSpecification",
dayOfWeek: ["Saturday", "Sunday"],
opens: "10:00",
closes: "16:00",
},
]}
```
**24/7 Operation:**
```tsx
openingHoursSpecification={{
"@type": "OpeningHoursSpecification",
dayOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
opens: "00:00",
closes: "23:59",
}}
```
**Closed on Specific Days:**
```tsx
openingHoursSpecification={{
"@type": "OpeningHoursSpecification",
dayOfWeek: "Sunday",
opens: "00:00",
closes: "00:00",
}}
```
**Seasonal Hours:**
```tsx
openingHoursSpecification={{
"@type": "OpeningHoursSpecification",
dayOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
opens: "10:00",
closes: "18:00",
validFrom: "2024-06-01",
validThrough: "2024-09-30",
}}
```
#### Best Practices
1. **Use specific business types**: Use the most specific LocalBusiness subtype (e.g., "Restaurant" instead of "LocalBusiness")
2. **Multiple types**: For businesses that fit multiple categories, use an array (e.g., `["Restaurant", "BarOrPub"]`)
3. **Complete address**: Provide as many address fields as possible for better local SEO
4. **High-quality images**: Include multiple images with different aspect ratios (16:9, 4:3, 1:1)
5. **Accurate coordinates**: Use at least 5 decimal places for latitude and longitude
6. **Opening hours**: Be precise with opening hours and include seasonal variations
7. **Department naming**: Include the main store name with department name (e.g., "Store Name - Pharmacy")
8. **Price range**: Keep under 100 characters; use standard symbols ($, $$, $$$) or ranges
[↑ Back to Components](#-components-by-category)
### MerchantReturnPolicyJsonLd
The `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.
#### Basic Usage - Option A (Detailed Properties)
Use this pattern when you want to provide detailed return policy information:
```tsx
import { MerchantReturnPolicyJsonLd } from "next-seo";
;
```
#### Basic Usage - Option B (Link Only)
Use this pattern when you prefer to link to your return policy page:
```tsx
import { MerchantReturnPolicyJsonLd } from "next-seo";
;
```
#### Advanced Usage with All Features
```tsx
import { MerchantReturnPolicyJsonLd } from "next-seo";
;
```
#### Product-Level Return Policy
You can also specify return policies for individual products:
```tsx
import { ProductJsonLd } from "next-seo";
;
```
#### Organization-Level Return Policy
For online stores, specify a standard return policy at the organization level:
```tsx
import { OrganizationJsonLd } from "next-seo";
;
```
#### Props
| Property | Type | Description |
| ----------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- |
| **Option A Properties** |
| `applicableCountry` | `string \| string[]` | **Required** (Option A). Countries where products are sold |
| `returnPolicyCategory` | `string` | **Required** (Option A). Type of return policy |
| `merchantReturnDays` | `number` | Days for returns (required if finite window) |
| `returnPolicyCountry` | `string \| string[]` | Countries where returns are processed |
| `returnMethod` | `string \| string[]` | How items can be returned |
| `returnFees` | `string` | Type of return fees |
| `returnShippingFeesAmount` | `MonetaryAmount` | Shipping fee for returns |
| `refundType` | `string \| string[]` | Types of refunds available |
| `restockingFee` | `number \| MonetaryAmount` | Restocking fee (percentage or fixed) |
| `returnLabelSource` | `string` | How customers get return labels |
| `itemCondition` | `string \| string[]` | Acceptable return conditions |
| **Customer Remorse Properties** |
| `customerRemorseReturnFees` | `string` | Fees for change-of-mind returns |
| `customerRemorseReturnShippingFeesAmount` | `MonetaryAmount` | Shipping fee for remorse returns |
| `customerRemorseReturnLabelSource` | `string` | Label source for remorse returns |
| **Item Defect Properties** |
| `itemDefectReturnFees` | `string` | Fees for defective item returns |
| `itemDefectReturnShippingFeesAmount` | `MonetaryAmount` | Shipping fee for defect returns |
| `itemDefectReturnLabelSource` | `string` | Label source for defect returns |
| **Seasonal Override** |
| `returnPolicySeasonalOverride` | `SeasonalOverride \| SeasonalOverride[]` | Temporary policy changes |
| **Option B Property** |
| `merchantReturnLink` | `string` | URL to return policy page |
| **Component Properties** |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key for React rendering |
#### Return Policy Categories
- `https://schema.org/MerchantReturnFiniteReturnWindow` - Limited return period
- `https://schema.org/MerchantReturnNotPermitted` - No returns allowed
- `https://schema.org/MerchantReturnUnlimitedWindow` - Unlimited return period
#### Return Methods
- `https://schema.org/ReturnByMail` - Return by mail
- `https://schema.org/ReturnInStore` - Return in store
- `https://schema.org/ReturnAtKiosk` - Return at kiosk
#### Return Fees
- `https://schema.org/FreeReturn` - No charge for returns
- `https://schema.org/ReturnFeesCustomerResponsibility` - Customer pays for return
- `https://schema.org/ReturnShippingFees` - Specific shipping fee charged
#### Refund Types
- `https://schema.org/FullRefund` - Full monetary refund
- `https://schema.org/ExchangeRefund` - Exchange for same product
- `https://schema.org/StoreCreditRefund` - Store credit issued
#### Best Practices
1. **Choose the right option**: Use Option A for detailed policies, Option B for complex or frequently changing policies
2. **Specify all countries**: List all countries where your policy applies
3. **Different return scenarios**: Use customer remorse and item defect properties for different conditions
4. **Seasonal variations**: Use seasonal overrides for holiday return windows
5. **Product overrides**: Override organization-level policies for specific products when needed
6. **Clear fee structure**: Be transparent about any fees customers will incur
7. **Multiple return methods**: Offer multiple return options for customer convenience
8. **Accurate time windows**: Ensure merchantReturnDays matches your actual policy
[↑ Back to Components](#-components-by-category)
### MovieCarouselJsonLd
The `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.
#### Basic Usage - Summary Page Pattern
Use this pattern when you have separate detail pages for each movie:
```tsx
import { MovieCarouselJsonLd } from "next-seo";
;
```
#### All-in-One Page Pattern
Use this pattern when all movie information is on a single page:
```tsx
```
#### Advanced Example with All Features
```tsx
```
#### Props
| Property | Type | Description |
| ----------- | --------------------------------------------------- | ---------------------------------------------------------------- |
| `urls` | `Array` | **Required for summary pattern.** URLs to individual movie pages |
| `movies` | `MovieListItem[]` | **Required for all-in-one pattern.** Array of movie data |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key prop for React |
#### MovieListItem Properties
| Property | Type | Description |
| ----------------- | ---------------------------------------------------- | --------------------------------------------------------------- |
| `name` | `string` | **Required.** The name of the movie |
| `image` | `string \| ImageObject \| (string \| ImageObject)[]` | **Required.** Movie poster/image (6:9 aspect ratio recommended) |
| `url` | `string` | URL to the movie's page |
| `dateCreated` | `string` | Release date in ISO 8601 format |
| `director` | `string \| Person` | Movie director (accepts string or Person object) |
| `review` | `Review` | A review of the movie |
| `aggregateRating` | `AggregateRating` | Average rating based on multiple reviews |
#### Best Practices
1. **Mobile-only feature**: Movie carousels only appear on mobile devices in Google Search
2. **Image requirements**: Use 6:9 aspect ratio images (Google's requirement for movie carousels)
3. **High-quality images**: Images must be high resolution and properly formatted (.jpg, .png, .gif)
4. **Multiple images**: Consider providing multiple aspect ratios for better compatibility
5. **Complete movie data**: Include as many properties as possible for richer search results
6. **Consistent data**: All movies in the carousel must be from the same website
7. **URL structure**: For summary pages, ensure all URLs point to pages on the same domain
[↑ Back to Components](#-components-by-category)
### BreadcrumbJsonLd
The `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.
#### Basic Usage
```tsx
import { BreadcrumbJsonLd } from "next-seo";
export default function ProductPage() {
return (
<>
Wireless Headphones XYZ
{/* Product content */}
>
);
}
```
#### Multiple Breadcrumb Trails
Some pages can be reached through multiple paths. You can specify multiple breadcrumb trails:
```tsx
```
#### Advanced Example with Thing Objects
You can use Thing objects with `@id` instead of plain URL strings:
```tsx
```
#### Props
| Property | Type | Description |
| ---------------- | ------------------------ | ---------------------------------------------------------------- |
| `items` | `BreadcrumbListItem[]` | Array of breadcrumb items (required if not using multipleTrails) |
| `multipleTrails` | `BreadcrumbListItem[][]` | Array of breadcrumb trails (required if not using items) |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key prop for React |
**BreadcrumbListItem Type:**
| Property | Type | Description |
| -------- | ----------------------------- | ------------------------------------------------------ |
| `name` | `string` | **Required.** The title of the breadcrumb |
| `item` | `string \| { "@id": string }` | URL or Thing object (optional for the last breadcrumb) |
#### Best Practices
1. **Omit the last item's URL**: The last breadcrumb (current page) typically shouldn't have an `item` property
2. **Use logical hierarchy**: Breadcrumbs should represent a typical user path, not necessarily mirror URL structure
3. **Keep names concise**: Use clear, descriptive names that help users understand the hierarchy
4. **Multiple trails**: Use `multipleTrails` when a page can be logically reached through different paths
5. **Include home**: Start trails from a logical entry point (often "Home") but it's not required
6. **Avoid duplicates**: Each trail should represent a unique path to the page
7. **Match visual breadcrumbs**: The structured data should match the breadcrumbs shown on your page
[↑ Back to Components](#-components-by-category)
### CarouselJsonLd
The `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.
#### Basic Usage
**Summary Page Pattern (URLs only):**
```tsx
import { CarouselJsonLd } from "next-seo";
// Simple array of URLs
// With custom positions
```
**All-in-One Page Pattern (Full Data):**
```tsx
import { CarouselJsonLd } from "next-seo";
// Course Carousel
// Movie Carousel
// Recipe Carousel
// Restaurant Carousel
```
#### Advanced Examples
**Recipe Carousel with Full Details:**
```tsx
```
**Restaurant Carousel with Opening Hours:**
```tsx
```
#### Props
| Property | Type | Description |
| ------------- | ----------------------------------------------------------------- | ------------------------------------------------ |
| `urls` | `SummaryPageItem[]` | Array of URLs for summary page pattern |
| `contentType` | `"Course" \| "Movie" \| "Recipe" \| "Restaurant"` | Type of content in the carousel (for all-in-one) |
| `items` | `CourseItem[] \| MovieItem[] \| RecipeItem[] \| RestaurantItem[]` | Array of items matching the content type |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key prop for React |
**SummaryPageItem Type:**
| Type | Description |
| ------------------------------------ | --------------------------------- |
| `string` | Simple URL string |
| `{ url: string; position?: number }` | URL with optional custom position |
#### Best Practices
1. **Choose the right pattern**:
- Use **summary page pattern** when you have separate detail pages for each item
- Use **all-in-one pattern** when all content is on a single page
2. **Consistent content types**: All items in a carousel must be of the same type (e.g., all recipes or all movies)
3. **Required images**:
- Movies require at least one image
- Recipes should include images for better visibility
- Use multiple aspect ratios when possible
4. **Position numbering**:
- Positions start at 1, not 0
- If not specified, positions are auto-assigned sequentially
5. **URL structure**: For summary pages, ensure all URLs point to pages on the same domain
6. **Rich content**: Include as much relevant information as possible for better search results
7. **Validation**: Test your structured data with Google's Rich Results Test
[↑ Back to Components](#-components-by-category)
### CourseJsonLd
The `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.
#### Basic Usage
**Single Course:**
```tsx
import { CourseJsonLd } from "next-seo";
;
```
**Course List:**
```tsx
import { CourseJsonLd } from "next-seo";
// Summary page pattern - just URLs
// All-in-one page pattern - full course data
```
#### Props
**Single Course Props:**
| Property | Type | Description |
| ------------- | ------------------------------------------------------- | --------------------------------------------------------------------- |
| `type` | `"single"` | Optional. Explicitly sets single course pattern |
| `name` | `string` | **Required.** The title of the course |
| `description` | `string` | **Required.** A description of the course (60 char limit for display) |
| `url` | `string` | The URL of the course page |
| `provider` | `string \| Organization \| Omit` | The organization offering the course |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key for React reconciliation |
**Course List Props:**
| Property | Type | Description |
| ----------- | -------------------------------------------------- | ------------------------------------------ |
| `type` | `"list"` | **Required.** Sets the course list pattern |
| `urls` | `(string \| { url: string; position?: number })[]` | URLs for summary page pattern |
| `courses` | `CourseListItem[]` | Full course data for all-in-one pattern |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key for React reconciliation |
#### Advanced Example
```tsx
import { CourseJsonLd } from "next-seo";
export default function CourseCatalogPage() {
return (
<>
Our Course Catalog
{/* Your course list UI */}
>
);
}
```
#### Best Practices
1. **Minimum of 3 courses**: Google requires at least 3 courses for course list rich results
2. **Consistent provider**: Use the same format for provider across all courses
3. **Description length**: Keep descriptions under 60 characters for optimal display
4. **Valid URLs**: Ensure all course URLs are accessible and on the same domain
5. **Choose the right pattern**:
- Use **summary page** pattern when courses have their own detail pages
- Use **all-in-one** pattern when all course information is on a single page
6. **Avoid promotional content**: Don't include prices, discounts, or marketing language in course names
### EventJsonLd
The `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.
#### Basic Usage
```tsx
import { EventJsonLd } from "next-seo";
;
```
#### Standard Event Example
```tsx
```
#### Event Status Examples
##### Cancelled Event
```tsx
```
##### Rescheduled Event
```tsx
```
#### Props
| Property | Type | Description |
| ------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `name` | `string` | **Required.** The full title of the event |
| `startDate` | `string` | **Required.** Start date/time in ISO-8601 format |
| `location` | `string \| Place` | **Required.** Event venue (string or Place object) |
| `endDate` | `string` | End date/time in ISO-8601 format |
| `description` | `string` | Detailed description of the event |
| `eventStatus` | `EventStatusType` | Status: EventScheduled (default), EventCancelled, EventPostponed, EventRescheduled |
| `image` | `string \| ImageObject \| (string \| ImageObject)[]` | Event images (recommended: multiple aspect ratios) |
| `offers` | `Offer \| Offer[]` | Ticket/pricing information |
| `performer` | `string \| Person \| PerformingGroup \| array` | Performers at the event |
| `organizer` | `string \| Person \| Organization` | Event host/organizer |
| `previousStartDate` | `string \| string[]` | Previous date(s) for rescheduled events |
| `url` | `string` | URL of the event page |
| `scriptId` | `string` | Custom ID for the script tag |
| `scriptKey` | `string` | Custom key prop for React |
#### Offer Type
| Property | Type | Description |
| --------------- | -------- | ------------------------------------------------ |
| `url` | `string` | URL to purchase tickets |
| `price` | `number` | Lowest available price (use 0 for free events) |
| `priceCurrency` | `string` | 3-letter ISO 4217 currency code (e.g., "USD") |
| `availability` | `string` | Availability status (InStock, SoldOut, PreOrder) |
| `validFrom` | `string` | Date/time when tickets go on sale |
#### Best Practices
1. **Date/Time Format**: Always use ISO-8601 format with timezone offset (e.g., `2025-07-21T19:00-05:00`)
2. **Day-long Events**: For all-day events, use date only format (e.g., `2025-07-04`)
3. **Location Details**: Provide complete address information for better discoverability
4. **Multiple Images**: Include images in different aspect ratios (16:9, 4:3, 1:1) for various display contexts
5. **Event Status**: Keep original dates when cancelling/postponing; only update the `eventStatus`
6. **Free Events**: Set `price: 0` for events without charge
7. **Multiple Performers**: Use an array when listing multiple artists or speakers
8. **Rescheduled Events**: Always include `previousStartDate` when using `EventRescheduled` status
#### Date and Time Guidelines
- **Include timezone**: Specify UTC/GMT offset (e.g., `-05:00` for EST)
- **Multi-day events**: Set both `startDate` and `endDate`
- **Unknown end time**: Omit `endDate` rather than guessing
- **Date-only format**: Use for all-day events (e.g., festivals)
Example timezone handling:
```tsx
// New York event during standard time
startDate: "2025-12-21T19:00:00-05:00";
// California event during daylight saving time
startDate: "2025-07-21T19:00:00-07:00";
// All-day event
startDate: "2025-07-04";
endDate: "2025-07-04";
```
### FAQJsonLd
The `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.
> **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.
#### Basic Usage
```tsx
import { FAQJsonLd } from "next-seo";
export default function FAQPage() {
return (
<>
Frequently Asked Questions
{/* Your FAQ content */}
>
);
}
```
#### Advanced Example with HTML Content
FAQ answers support HTML content including links, lists, and formatting:
```tsx
You'll need to provide the following documents:
All documents must be submitted within 30 days of application.
`,
},
{
question: "How long does the application process take?",
answer:
"The typical processing time is 7-10 business days from the date we receive all required documents.
",
},
]}
scriptId="faq-structured-data"
/>
```
#### Different Input Formats
The component supports multiple input formats for flexibility:
```tsx
// Simple question/answer format (recommended)
// Schema.org name/acceptedAnswer format
// With Answer object
```
#### Props
| Property | Type | Description |
| ----------- | ----------------- | --------------------------------------------------------------------------------------- |
| `questions` | `QuestionInput[]` | **Required.** Array of questions and answers. See input formats below. |
| `scriptId` | `string` | Optional. Sets the `id` attribute on the script tag. |
| `scriptKey` | `string` | Optional. Sets the `data-testid` attribute on the script tag. Defaults to "faq-jsonld". |
#### Question Input Formats
The `questions` array accepts several formats:
1. **Simple object** (recommended):
```tsx
{ question: "string", answer: "string" }
```
2. **Schema.org format**:
```tsx
{ name: "string", acceptedAnswer: "string" }
```
3. **Full Answer object**:
```tsx
{
name: "string",
acceptedAnswer: {
"@type": "Answer",
text: "string"
}
}
```
#### Best Practices
1. **Include complete Q&A**: Each question and answer should contain the full text that appears on your page
2. **Use HTML wisely**: Google supports these HTML tags in answers: `