From 252a12eda43f8eee7d68bc3c7f22c2fc22619c48 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Sun, 24 Nov 2024 18:04:16 +1100 Subject: [PATCH] feat: improved identity configuration (#66) --- build.config.ts | 8 + .../0.getting-started/1.installation.md | 2 +- docs/content/2.guides/1.default-schema-org.md | 2 +- docs/content/2.guides/1.quick-setup.md | 90 --- docs/content/2.guides/1.setup-identity.md | 534 ++++++++++++++++++ package.json | 7 +- schema.d.ts | 1 + src/module.ts | 4 +- src/runtime/app/utils/shared.ts | 15 +- src/schema.ts | 1 + 10 files changed, 560 insertions(+), 104 deletions(-) create mode 100644 build.config.ts delete mode 100644 docs/content/2.guides/1.quick-setup.md create mode 100644 docs/content/2.guides/1.setup-identity.md create mode 100644 schema.d.ts create mode 100644 src/schema.ts diff --git a/build.config.ts b/build.config.ts new file mode 100644 index 0000000..b4e0c01 --- /dev/null +++ b/build.config.ts @@ -0,0 +1,8 @@ +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + declaration: true, + entries: [ + { input: 'src/schema', name: 'schema' }, + ], +}) diff --git a/docs/content/0.getting-started/1.installation.md b/docs/content/0.getting-started/1.installation.md index e68cbf4..ab11d25 100644 --- a/docs/content/0.getting-started/1.installation.md +++ b/docs/content/0.getting-started/1.installation.md @@ -34,4 +34,4 @@ Schema.org. Other suggestions: -- [Setup Your Identity](/docs/schema-org/guides/quick-setup) +- [Setup Your Identity](/docs/schema-org/guides/setup-identity) diff --git a/docs/content/2.guides/1.default-schema-org.md b/docs/content/2.guides/1.default-schema-org.md index d68f1f6..99319c0 100644 --- a/docs/content/2.guides/1.default-schema-org.md +++ b/docs/content/2.guides/1.default-schema-org.md @@ -98,4 +98,4 @@ export default defineNuxtConfig({ ## Configuring Identity -Please see the [Setup Identity](/docs/schema-org/guides/quick-setup) guide for more information on configuring your identity. +Please see the [Setup Identity](/docs/schema-org/guides/setup-identity) guide for more information on configuring your identity. diff --git a/docs/content/2.guides/1.quick-setup.md b/docs/content/2.guides/1.quick-setup.md deleted file mode 100644 index f07abd2..0000000 --- a/docs/content/2.guides/1.quick-setup.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Setup Identity -description: Set up Schema.org on your Nuxt app quickly. ---- - -## Introduction - -By default, a Nuxt plugin is registered in your app that will register the root nodes for a -`WebSite` and `WebPage` for you. - -These are configured using [Nuxt Site Config](/docs/site-config/getting-started/how-it-works), - -:ModuleCard{slug="site-config" class="w-1/2"} - -The only configuration you may need to provide is the identity of your site. - -## Selecting An Identity - -Selecting an identity makes sure the [Default Schema.org](/docs/schema-org/guides/default-schema-org) is correctly linked -to the author of the site. - -There are two types of identities you can use: [Organisation](https://unhead.unjs.io/schema-org/recipes/identity#organization) and [Person](https://unhead.unjs.io/schema-org/recipes/identity#person)`. - -If the choice isn't clear, you can use the `Organization` identity as a default or read the [Choosing an identity](https://unhead.unjs.io/schema-org/recipes/identity) docs for more information. - -## Setting Identity - -The simplest way to set up your identity is to set it in your `nuxt.config` using a string: - -::code-block - -```ts [Organization] -export default defineNuxtConfig({ - schemaOrg: { - identity: 'Organization' - } -}) -``` - -```ts [Person] -export default defineNuxtConfig({ - schemaOrg: { - identity: 'Person' - } -}) -``` - -:: - -### Providing Extra Identity Data - -It's recommended to provide more information about your identity, such as the name, URL, logo and social media links. - -::code-block - -```ts [Organization] -// example for nuxt.com -export default defineNuxtConfig({ - schemaOrg: { - identity: { - type: 'Organization', - name: 'NuxtJS', - logo: '/logo.png', // will resolve to canonical URL + /logo.png - sameAs: [ - 'https://x.com/nuxt_js', - 'https://www.linkedin.com/showcase/nuxt-framework/', - 'https://github.com/nuxt' - ] - } - } -}) -``` - -```ts [Person] -// example for harlanzw.com -export default defineNuxtConfig({ - schemaOrg: { - identity: { - type: 'Person', - name: 'Harlan Wilton', - image: '/profile.jpg', - sameAs: [ - 'https://x.com/harlan_zw', - 'https://github.com/harlan-zw', - 'https://harlanzw.com' - ] - } - } -}) -``` diff --git a/docs/content/2.guides/1.setup-identity.md b/docs/content/2.guides/1.setup-identity.md new file mode 100644 index 0000000..913650a --- /dev/null +++ b/docs/content/2.guides/1.setup-identity.md @@ -0,0 +1,534 @@ +--- +title: Setup Identity +description: Improve your Schema.org by providing the identity of your site. +--- + +## Introduction + +Providing an identify will be link the [Default Schema.org](/docs/schema-org/guides/default-schema-org) to the author of the site +and for `Organization` or `LocalBusiness` nodes may help with [Rich Results](https://developers.google.com/search/docs/appearance/structured-data/organization). + +Schema.org Identity + +## Choosing an Identity + +Your choices fore identity are: +- [`Person`{lang="ts"}](#person) +- [`Organization`{lang="ts"}](#organization) +- [`OnlineStore`{lang="ts"}](#onlinestore) +- [`LocalBusiness`{lang="ts"}](#localbusiness) + +### Person + +A `Person`{lang="ts"} identity should be used when your website is about a person, a personal brand or a personal blog. + +Example: [harlanzw.com](https://harlanzw.com), [antfu.me](antfu.me) + +```ts [Person] +import { definePerson } from 'nuxt-schema-org/schema' + +export default defineNuxtConfig({ + schemaOrg: { + identity: definePerson({ + type: 'Person', + + // Basic Information, if applicable + name: 'Dr. Sarah Chen', + givenName: 'Sarah', + familyName: 'Chen', + additionalName: 'J.', // middle name or other additional names + alternateName: 'Sarah J. Chen', + + // Profile Information, if applicable + image: '/profile-photo.jpg', + description: 'AI researcher and technical author specializing in machine learning and neural networks', + jobTitle: 'Principal AI Researcher', + + // Contact & Social, if applicable + email: 'sarah.chen@example.com', + url: 'https://sarahchen.dev', + sameAs: [ + 'https://twitter.com/sarahchen', + 'https://github.com/sarahchen', + 'https://linkedin.com/in/sarahchen', + 'https://scholar.google.com/citations?user=sarahchen' + ], + + // Professional Details, if applicable + worksFor: { + '@type': 'Organization', + 'name': 'Tech Research Labs', + 'url': 'https://techresearchlabs.com' + }, + }) + } +}) +``` + +### Organization + +The `Organization`{lang="ts"} identity should be used when your website is about a company, a brand, a non-profit or a community. However, +it should also be used as the catch-all identity when the other options don't fit. + +```ts [Organization] +import { defineOrganization } from 'nuxt-schema-org/schema' + +export default defineNuxtConfig({ + schemaOrg: { + identity: defineOrganization({ + type: 'Organization', + + // Basic Information + name: 'TechCorp Solutions', + alternateName: 'TechCorp', + description: 'Leading provider of enterprise software solutions and cloud services', + url: 'https://techcorp.com', + logo: '/logo.png', + + // Address Information, if applicable + address: { + '@type': 'PostalAddress', + 'streetAddress': '100 Innovation Drive, Suite 400', + 'addressLocality': 'Silicon Valley', + 'addressRegion': 'CA', + 'postalCode': '94025', + 'addressCountry': 'US' + }, + + // Contact Information, if applicable + email: 'info@techcorp.com', + telephone: '+1-650-555-0123', + contactPoint: { + '@type': 'ContactPoint', + 'telephone': '+1-650-555-0124', + 'email': 'support@techcorp.com' + }, + + // Business Details, if applicable + foundingDate: '2010-01-15', + numberOfEmployees: { + '@type': 'QuantitativeValue', + 'minValue': 500, + 'maxValue': 999 + }, + + // Social and External Links, if applicable + sameAs: [ + 'https://twitter.com/techcorp', + 'https://www.linkedin.com/company/techcorp', + 'https://www.facebook.com/techcorp' + ], + + // Business Identifiers, if applicable + legalName: 'TechCorp Solutions Inc.', + taxID: '12-3456789', + vatID: 'GB123456789', + duns: '12-345-6789', + iso6523Code: '0060:123456789', + naics: '541512', + + // Return Policy, if applicable + hasMerchantReturnPolicy: { + '@type': 'MerchantReturnPolicy', + 'name': 'Standard Return Policy', + 'inStoreReturnsOffered': true, + 'returnPolicyCategory': 'https://schema.org/MerchantReturnFiniteReturnWindow', + 'returnPolicyCountry': 'US', + 'returnWindow': { + '@type': 'BusinessDaysSpecification', + 'numberOfDays': 30 + } + } + }) + } +}) +``` + +### LocalBusiness + +The `LocalBusiness`{lang="ts"} identity should be used when your website is about a local business, a store, a restaurant or a service. It +must have a physical address associated with it. + +Some examples of `LocalBusiness`{lang="ts}: : `Restaurant`, `HealthAndBeautyBusiness`, `ProfessionalService`, `FinancialService`, `MedicalBusiness`, etc... + +Google recommends using the most specific type of `LocalBusiness`{lang="ts"} that fits your business, check the list +of [subtypes](https://schema.org/LocalBusiness#subtypes) to find the most appropriate. + +If you need to use dynamic data, you can use the `defineLocalBusiness`{lang="ts"} function to define the identity +within your app.vue. + +::code-group + +```ts [LocalBusiness - Static] +import { defineLocalBusiness } from 'nuxt-schema-org/schema' + +export default defineNuxtConfig({ + schemaOrg: { + identity: defineLocalBusiness({ + '@type': '...', // Choose from https://schema.org/LocalBusiness#subtypes + + // Basic Information (Required) + 'name': 'The Coastal Kitchen', + 'description': 'Farm-to-table restaurant specializing in sustainable seafood and seasonal ingredients', + 'url': 'https://thecoastalkitchen.com', + + // Location (Required) + 'address': { + streetAddress: '742 Oceanview Boulevard, Suite 100', + addressLocality: 'Santa Cruz', + addressRegion: 'CA', + postalCode: '95060', + addressCountry: 'US' + }, + + // Precise Geographic Location, if applicable + 'geo': { + '@type': 'GeoCoordinates', + 'latitude': '36.9741', + 'longitude': '-122.0308' + }, + + // Contact Information, if applicable + 'telephone': '+1-831-555-0123', + 'email': 'hello@thecoastalkitchen.com', + + // Hours of Operation, if applicable + 'openingHoursSpecification': [ + { + dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday'], + opens: '11:30:00', + closes: '22:00:00' + }, + { + dayOfWeek: ['Friday', 'Saturday'], + opens: '11:30:00', + closes: '23:00:00' + }, + { + dayOfWeek: 'Sunday', + opens: '10:00:00', // Sunday Brunch + closes: '21:00:00' + } + ], + + // Business Details, if applicable + 'priceRange': '$$$', // $, $$, $$$, or $$$$ + 'servesCuisine': [ + 'Seafood', + 'California', + 'Farm-to-table' + ], + + // Menu (for restaurants) + 'menu': 'https://thecoastalkitchen.com/menu', + + // Images, if applicable + 'image': [ + 'https://thecoastalkitchen.com/images/storefront.jpg', + 'https://thecoastalkitchen.com/images/interior.jpg', + 'https://thecoastalkitchen.com/images/food.jpg' + ], + 'logo': '/logo.png', + + // Payment Options, if applicable + 'paymentAccepted': [ + 'Cash', + 'Credit Card', + 'Cryptocurrency' + ], + 'currenciesAccepted': 'USD', + + // Additional Business Details, if applicable + 'isAccessibleForDisabled': true, + 'amenityFeature': [ + { + '@type': 'LocationFeatureSpecification', + 'name': 'Parking', + 'value': true + }, + { + '@type': 'LocationFeatureSpecification', + 'name': 'Wheelchair Accessible', + 'value': true + }, + { + '@type': 'LocationFeatureSpecification', + 'name': 'Outdoor Seating', + 'value': true + } + ], + + // Social Links, if applicable + 'sameAs': [ + 'https://www.facebook.com/coastalkitchen', + 'https://instagram.com/thecoastalkitchen', + 'https://twitter.com/coastalkitchen' + ] + }), + } +}) +``` + +```vue [LocalBusiness - Dynamic] + +``` + +:: + +### OnlineStore + +The `OnlineStore`{lang="ts"} identity should be used for ecommerce sites. + +```ts [OnlineStore] +import { defineOrganization } from 'nuxt-schema-org/schema' + +export default defineNuxtConfig({ + schemaOrg: { + identity: defineOrganization({ + '@type': ['Organization', 'Store', 'OnlineStore'], + + // Basic Information + 'name': 'ModernHome', + 'alternateName': 'Modern Home Decor', + 'description': 'Contemporary furniture and home decor with worldwide shipping. Specializing in minimalist Scandinavian design.', + 'url': 'https://modernhome.com', + 'logo': '/logo.png', + + // Contact Information, if applicable + 'email': 'support@modernhome.com', + 'telephone': '+1-888-555-0123', + 'contactPoint': [ + { + '@type': 'ContactPoint', + 'contactType': 'customer service', + 'telephone': '+1-888-555-0123', + 'email': 'support@modernhome.com', + 'availableLanguage': ['English', 'Spanish'], + 'hoursAvailable': { + '@type': 'OpeningHoursSpecification', + 'dayOfWeek': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], + 'opens': '09:00:00', + 'closes': '18:00:00' + } + }, + { + '@type': 'ContactPoint', + 'contactType': 'sales', + 'telephone': '+1-888-555-0124', + 'email': 'sales@modernhome.com' + } + ], + + // Business Details, if applicable + 'foundingDate': '2015-01-01', + 'numberOfEmployees': { + '@type': 'QuantitativeValue', + 'value': 85 + }, + + // Legal Information, if applicable + 'legalName': 'ModernHome Inc.', + 'taxID': '47-1234567', + 'vatID': 'EU123456789', + + // Business Address (headquarters/returns), if applicable + 'address': { + '@type': 'PostalAddress', + 'streetAddress': '100 Commerce Way, Suite 300', + 'addressLocality': 'Portland', + 'addressRegion': 'OR', + 'postalCode': '97201', + 'addressCountry': 'US' + }, + + // Return Policy, if applicable + 'hasMerchantReturnPolicy': { + '@type': 'MerchantReturnPolicy', + 'name': 'Standard Return Policy', + 'inStoreReturnsOffered': false, + 'merchantReturnDays': '30', + 'returnPolicyCategory': 'https://schema.org/MerchantReturnFiniteReturnWindow', + 'returnMethod': ['ReturnByMail'], + 'returnFees': 'https://schema.org/FreeReturn', + 'returnPolicyCountry': { + '@type': 'Country', + 'name': ['US', 'CA', 'GB', 'AU', 'NZ'] + } + }, + + // Shipping Policy, if applicable + 'shippingDetails': { + '@type': 'OfferShippingDetails', + 'shippingRate': { + '@type': 'MonetaryAmount', + 'value': '0', + 'currency': 'USD' + }, + 'shippingDestination': { + '@type': 'DefinedRegion', + 'addressCountry': ['US', 'CA', 'GB', 'AU', 'NZ'] + }, + 'deliveryTime': { + '@type': 'ShippingDeliveryTime', + 'handlingTime': { + '@type': 'QuantitativeValue', + 'minValue': 1, + 'maxValue': 2, + 'unitCode': 'DAY' + }, + 'transitTime': { + '@type': 'QuantitativeValue', + 'minValue': 3, + 'maxValue': 7, + 'unitCode': 'DAY' + } + } + }, + + // Payment Methods, if applicable + 'paymentAccepted': [ + 'Credit Card', + 'PayPal', + 'Apple Pay', + 'Google Pay', + 'Shop Pay' + ], + 'currenciesAccepted': ['USD', 'EUR', 'GBP', 'CAD', 'AUD'], + + // Social Media & External Links, if applicable + 'sameAs': [ + 'https://facebook.com/modernhome', + 'https://instagram.com/modernhome', + 'https://pinterest.com/modernhome', + 'https://twitter.com/modernhome' + ], + + // Trust Indicators, if applicable + 'hasCredential': [ + { + '@type': 'EducationalOccupationalCredential', + 'credentialCategory': 'BBB Rating A+', + 'url': 'https://www.bbb.org/modernhome' + }, + { + '@type': 'EducationalOccupationalCredential', + 'credentialCategory': 'Certified B Corporation', + 'url': 'https://www.bcorporation.net/modernhome' + } + ], + + // Aggregate Ratings, if applicable + 'aggregateRating': { + '@type': 'AggregateRating', + 'ratingValue': '4.8', + 'reviewCount': '12459', + 'bestRating': '5', + 'worstRating': '1' + }, + + // Customer Service Features, if applicable + 'hasOfferCatalog': { + '@type': 'OfferCatalog', + 'name': 'ModernHome Product Catalog', + 'url': 'https://modernhome.com/products' + }, + + // Additional Business Properties, if applicable + 'slogan': 'Design for Modern Living', + 'keywords': [ + 'modern furniture', + 'scandinavian design', + 'home decor', + 'minimalist furniture', + 'contemporary home' + ], + + // Business Hours (Customer Service), if applicable + 'openingHoursSpecification': [ + { + '@type': 'OpeningHoursSpecification', + 'dayOfWeek': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], + 'opens': '09:00:00', + 'closes': '18:00:00' + } + ] + }), + } +}) +``` + +## Recipes + +It's recommended to provide as much information about your identity as possible, here are some recipes. + +### Social Media Profiles + +::code-block + +```ts [Organization] +// example for nuxt.com +export default defineNuxtConfig({ + schemaOrg: { + identity: { + type: 'Organization', + name: 'NuxtJS', + logo: '/logo.png', // will resolve to canonical URL + /logo.png + sameAs: [ + 'https://x.com/nuxt_js', + 'https://www.linkedin.com/showcase/nuxt-framework/', + 'https://github.com/nuxt' + ] + } + } +}) +``` + +```ts [Person] +// example for harlanzw.com +export default defineNuxtConfig({ + schemaOrg: { + identity: { + type: 'Person', + name: 'Harlan Wilton', + image: '/profile.jpg', + sameAs: [ + 'https://x.com/harlan_zw', + 'https://github.com/harlan-zw', + 'https://harlanzw.com' + ] + } + } +}) +``` + +```ts [LocalBusiness] +// local coffee shop +export default defineNuxtConfig({ + schemaOrg: { + identity: { + type: 'LocalBusiness', + name: 'Coffee Shop', + logo: '/logo.png', // will resolve to canonical URL + /logo.png + sameAs: [ + 'https://x.com/coffee_shop', + 'https://www.facebook.com/coffee_shop', + 'https://www.yelp.com/coffee_shop' + ] + } + } +}) +``` + +:: diff --git a/package.json b/package.json index dacda87..434189e 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,17 @@ "types": "./dist/types.d.ts", "import": "./dist/module.mjs", "require": "./dist/module.cjs" + }, + "./schema": { + "types": "./dist/schema.d.ts", + "import": "./dist/schema.mjs" } }, "main": "./dist/module.cjs", "types": "./dist/types.d.ts", "files": [ - "dist" + "dist", + "schema.d.ts" ], "scripts": { "lint": "eslint . --fix", diff --git a/schema.d.ts b/schema.d.ts new file mode 100644 index 0000000..b0a68d1 --- /dev/null +++ b/schema.d.ts @@ -0,0 +1 @@ +export * from './dist/schema' diff --git a/src/module.ts b/src/module.ts index 068547e..10bb8ac 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,6 +1,6 @@ import type { NuxtModule } from '@nuxt/schema' import type { DataKeys, ScriptBase, TagUserProperties } from '@unhead/schema' -import type { OrganizationSimple, PersonSimple } from '@unhead/schema-org' +import type { LocalBusinessSimple, OrganizationSimple, PersonSimple } from '@unhead/schema-org' import type { ModuleRuntimeConfig } from './runtime/types' import { addComponent, @@ -26,7 +26,7 @@ export interface ModuleOptions { /** * The identity of the site. */ - identity?: 'Person' | 'Organization' | OrganizationSimple | PersonSimple + identity?: 'Person' | 'Organization' | 'LocalBusiness' | OrganizationSimple | PersonSimple | LocalBusinessSimple /** * Whether the module should be loaded. * diff --git a/src/runtime/app/utils/shared.ts b/src/runtime/app/utils/shared.ts index d372d53..e147cb2 100644 --- a/src/runtime/app/utils/shared.ts +++ b/src/runtime/app/utils/shared.ts @@ -1,13 +1,14 @@ -import type { MetaInput as _MetaInput, MetaInput, Organization, Person } from '@unhead/schema-org' +import type { MetaInput as _MetaInput, MetaInput } from '@unhead/schema-org' import type { NuxtApp } from 'nuxt/app' import { createSitePathResolver, useSiteConfig, } from '#imports' -import { defineOrganization, definePerson, SchemaOrgUnheadPlugin, useSchemaOrg } from '@unhead/schema-org' +import { SchemaOrgUnheadPlugin, useSchemaOrg } from '@unhead/schema-org' import { injectHead } from '@unhead/vue' import { defu } from 'defu' import { useRoute } from 'nuxt/app' +import { camelCase } from 'scule' import { withoutTrailingSlash } from 'ufo' import { computed } from 'vue' import { useSchemaOrgConfig } from './config' @@ -56,7 +57,7 @@ export function maybeAddIdentitySchemaOrg() { const siteConfig = useSiteConfig() if (config.identity || siteConfig.identity) { const identity = config.identity || siteConfig.identity - let identityPayload: Person | Organization = { + let identityPayload: Record = { name: siteConfig.name, url: siteConfig.url, } @@ -64,7 +65,6 @@ export function maybeAddIdentitySchemaOrg() { if (typeof identity !== 'string') { identityPayload = defu(identity, identityPayload) identityType = identity.type - // Remove type from object to avoid invalid markup delete identityPayload.type } @@ -80,10 +80,7 @@ export function maybeAddIdentitySchemaOrg() { `https://twitter.com/${id}`, ] } - useSchemaOrg([ - identityType === 'Person' - ? definePerson(identityPayload) - : defineOrganization(identityPayload), - ]) + identityPayload._resolver = identityPayload._resolver || camelCase(identityType) + useSchemaOrg([identityPayload]) } } diff --git a/src/schema.ts b/src/schema.ts new file mode 100644 index 0000000..67bca09 --- /dev/null +++ b/src/schema.ts @@ -0,0 +1 @@ +export * from '@unhead/schema-org'