cmelda / json-ld
JSON-LD builder for standalone entities and graphs with automatic entity references
Fund package maintenance!
Requires
- php: >=8.3
- ext-json: *
Requires (Dev)
- arxeiss/coding-standards: ^0.14
- codeception/codeception: ^5.1
- codeception/module-asserts: ^3
- phpstan/phpstan: ^2.0
- rector/rector: ^2.0
README
PHP library for standalone JSON-LD entities and JSON-LD documents with an
@graph. Entities with an @id are linked by reference and automatically added
to the document graph once.
Installation
composer require cmelda/json-ld
Standalone entity
use Cmelda\JsonLd\Types\Person;
echo Person::make('https://example.com', 'person')
->setName('George')
->toScript();
Graph document
use Cmelda\JsonLd\Core\JsonLdDocument;
use Cmelda\JsonLd\Types\Article;
use Cmelda\JsonLd\Types\Person;
$person = Person::make('https://example.com', 'person')->setName('George');
$article = Article::make('https://example.com/article')
->setHeadline('Sample article')
->addAuthor($person)
->addEditor($person);
echo JsonLdDocument::make()->add($article)->toScript();
The article contains two {"@id":"https://example.com#person"} references, but
the full person is present in @graph only once.
Shared organization
use Cmelda\JsonLd\Core\JsonLdDocument;
use Cmelda\JsonLd\Types\Article;
use Cmelda\JsonLd\Types\Organization;
use Cmelda\JsonLd\Types\Person;
$organization = Organization::make('https://example.com', 'organization')
->setName('Company');
$person = Person::make('https://example.com', 'person')
->setName('George')
->setWorksFor($organization);
$article = Article::make('https://example.com/article')
->addAuthor($person)
->setPublisher($organization);
echo JsonLdDocument::make()->add($article)->toScript();
Every parent stores an @id reference and the shared organization appears in
@graph once.
Inline and linked images
use Cmelda\JsonLd\Types\ImageObject;
$inline = ImageObject::make()->setUrl('https://example.com/image.jpg');
$linked = ImageObject::make('https://example.com/image.jpg')
->setUrl('https://example.com/image.jpg');
An image without an @id is embedded inline. An image with an @id is linked
by reference and added to the graph.
Standalone rendering is rejected when an entity has linked dependencies. Render
the document in that case. Entities remain reusable after they have been added
to a JsonLdDocument.
FAQ page
use Cmelda\JsonLd\Types\Answer;
use Cmelda\JsonLd\Types\FAQPage;
use Cmelda\JsonLd\Types\Question;
$faqPage = FAQPage::make()
->addMainEntity(
Question::make()
->setName('How long is the warranty?')
->setAcceptedAnswer(Answer::make()->setText('The warranty is two years.')),
);
echo $faqPage->toScript();
The PHP helper AdditionalProperty renders the Schema.org type PropertyValue.
Use VideoObject for Schema.org video metadata.
Practical metadata helpers
The library includes small helpers for common structured data metadata without trying to wrap the whole Schema.org vocabulary:
sameAs:addSameAs()on anyThingtype- taxonomy:
DefinedTermandDefinedTermSet - audience:
AudienceandPeopleAudience - areas served:
setAreaServed()onOrganization,LocalBusiness,Product,OfferandContactPoint - product relations:
addIsAccessoryOrSparePartFor()andaddIsConsumableFor() - commerce shortcuts:
OfferShippingDetails::forCountry()andMerchantReturnPolicy::forCountry() - content about a thing:
addSubjectOf()acceptsCreativeWorkorEventand automatically adds the inverseaboutreference when the parent has an@id
Schema.org enumerations
For common Schema.org enum values, prefer library enums instead of raw strings:
use Cmelda\JsonLd\Enums\ItemAvailability;
use Cmelda\JsonLd\Enums\ItemCondition;
use Cmelda\JsonLd\Types\Offer;
use Cmelda\JsonLd\Types\Product;
$product = Product::make()
->setItemCondition(ItemCondition::New);
$offer = Offer::make()
->setAvailability(ItemAvailability::InStock);
The setters still accept a string URL for new Schema.org values that are not yet present in the library.
Offer price specification
Use priceSpecification when you need detailed offer pricing, unit pricing or
compound price components:
use Cmelda\JsonLd\Types\Offer;
use Cmelda\JsonLd\Types\UnitPriceSpecification;
$offer = Offer::make()
->addPriceSpecification(
UnitPriceSpecification::make()
->setPrice('49.90')
->setPriceCurrency('USD')
->setValueAddedTaxIncluded(true),
);
Video metadata
VideoObject maps common CMS fields to Schema.org VideoObject properties and
extends MediaObject, which extends CreativeWork, which extends Thing.
Attach videos to products, offers or other things through addSubjectOf():
use Cmelda\JsonLd\Types\Product;
use Cmelda\JsonLd\Types\VideoObject;
$product = Product::fromUrl('https://example.com/products/widget')
->setName('Widget')
->addSubjectOf(
VideoObject::make('https://example.com/products/widget', 'video')
->setTitle('Widget overview', 'en-US')
->setThumbnailUrl('https://example.com/video.jpg')
->setUploadDate('2026-02-04T10:00:00+01:00'),
);
Product.subjectOf contains a reference to the video and the VideoObject
contains about with a reference back to the product.
Common video fields:
- localized video title fields: use
setTitle($title, $language) - localized video descriptions: use
setDescription($description)withsetInLanguage($language) thumbnail_url: usesetThumbnailUrl()upload_date: usesetUploadDate()duration_seconds: usesetDurationSeconds(), rendered as ISO 8601 durationcontent_url: usesetContentUrl()embed_url: usesetEmbedUrl()- publisher: use
setPublisher() - author: use
addAuthor() - media format and dimensions: use
setEncodingFormat(),setWidth()andsetHeight() - transcript/captions: use
setTranscript()andsetCaption() - license/copyright: use
setLicense(),setCopyrightHolder()andsetCopyrightYear() video_source: usesetVideoSource(), rendered asadditionalPropertyactive: usesetActive(), rendered asadditionalPropertylanguage: usesetInLanguage()
Factory fromUrl()
fromUrl() is an explicit way to create an @id from a URL. setUrl() only
sets the url property and never changes @id.
use Cmelda\JsonLd\Types\Person;
$person = Person::fromUrl('https://example.com/authors/alex')
->setName('Alex');
Use make($url, $fragment) when you already have a canonical URL and want a
specific fragment @id without manually concatenating #fragment:
use Cmelda\JsonLd\Types\LocalBusiness;
$store = LocalBusiness::make('https://example.com/store', 'store-ostrava')
->setName('Ostrava store');
Person, Organization, Product and LocalBusiness use a type-specific URL
fragment. Article, BlogPosting and TechArticle use their URL directly as
@id. A review uses Review::fromUrlAndId() because one product page can
contain multiple reviews.
Article types
Use Article for generic article content, BlogPosting for blog posts and
TechArticle for technical documentation or tutorials. BlogPosting follows
the Schema.org hierarchy through SocialMediaPosting and then Article:
use Cmelda\JsonLd\Types\BlogPosting;
use Cmelda\JsonLd\Types\TechArticle;
$post = BlogPosting::fromUrl('https://example.com/blog/widget-guide')
->setHeadline('Widget guide')
->setBlogSection('Guides');
$technical = TechArticle::fromUrl('https://example.com/docs/widget-api')
->setHeadline('Widget API')
->setProficiencyLevel('Beginner')
->addProgrammingLanguage('PHP');
Reviews
For a regular customer review, keep the customer, review and rating inline
without an @id:
use Cmelda\JsonLd\Types\Product;
use Cmelda\JsonLd\Types\Rating;
use Cmelda\JsonLd\Types\Review;
$review = Review::make()
->setAuthor('Jamie K.')
->setReviewBody('The widget works perfectly.')
->setReviewRating(Rating::make()->setRatingValue(5)->setBestRating(5));
$product = Product::fromUrl('https://example.com/products/nimbus-widget')
->setName('Nimbus Widget')
->addReview($review);
For an expert review, use Person::fromUrl() for the author's profile and
Review::fromUrlAndId() for the stable review identifier. The product contains
a review reference, the review contains an author reference and both entities
are added to @graph.
Validation fixture
The JSON files in tests/Support/Data/ contain complete
examples suitable for manual validation with online JSON-LD tools. The unit
test suite compares the fixtures with documents generated by the library.
More extensive PHP examples with their JSON outputs are available in
docs/.
Migrating from v1
Version 2 uses consistent addX() methods for properties with multiple values:
$product
->addImage('https://example.com/image.jpg')
->addReview($review);
$offer->addShippingDetails($shippingDetails);
$organization->addContactPoint($contactPoint);
Product::setBrand() accepts Brand|string and
Product::setAggregateRating() accepts AggregateRating. Entities remain
reusable and their @id can be changed after adding them to a document.
License
Distributed under the MIT License. See LICENSE.txt for more information.

