├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── renovate.json └── workflows │ ├── lock.yml │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc ├── .releaserc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── unsplash-selector.png ├── commitlint.config.cjs ├── lint-staged.config.cjs ├── package-lock.json ├── package.config.ts ├── package.json ├── sanity.json ├── src ├── components │ ├── Icon.tsx │ ├── Photo.styled.tsx │ ├── Photo.tsx │ ├── UnsplashAssetSource.styled.tsx │ └── UnsplashAssetSource.tsx ├── datastores │ └── unsplash.ts ├── index.ts └── types.ts ├── tsconfig.json ├── tsconfig.lib.json ├── tsconfig.settings.json └── v2-incompatible.js /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | charset= utf8 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | .eslintrc.js 3 | commitlint.config.js 4 | lib 5 | lint-staged.config.js 6 | package.config.ts 7 | tools/ 8 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | ecmaFeatures: { 6 | jsx: true, 7 | }, 8 | }, 9 | extends: [ 10 | 'sanity/react', // must come before sanity/typescript 11 | 'sanity/typescript', 12 | 'plugin:react-hooks/recommended', 13 | 'plugin:prettier/recommended', 14 | ], 15 | overrides: [ 16 | { 17 | files: ['*.{ts,tsx}'], 18 | rules: { 19 | 'no-undef': 0, 20 | }, 21 | }, 22 | ], 23 | plugins: ['simple-import-sort', 'prettier'], 24 | rules: { 25 | '@typescript-eslint/explicit-function-return-type': 0, 26 | '@typescript-eslint/explicit-module-boundary-types': 0, 27 | '@typescript-eslint/no-shadow': 'error', 28 | '@typescript-eslint/no-unused-vars': 1, 29 | 'simple-import-sort/imports': 'warn', 30 | 'simple-import-sort/exports': 'warn', 31 | 'no-unused-vars': 0, 32 | 'no-shadow': 'off', 33 | 'react/display-name': 0, 34 | 'react/jsx-no-bind': 0, 35 | 'react/jsx-handler-names': 0, 36 | camelcase: 0, 37 | 'symbol-description': 0, 38 | 'no-void': 0, 39 | }, 40 | settings: { 41 | 'import/ignore': ['\\.css$', '.*node_modules.*'], 42 | 'import/resolver': { 43 | node: { 44 | paths: ['src'], 45 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 46 | }, 47 | }, 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>sanity-io/renovate-config", 5 | "github>sanity-io/renovate-config:studio-v3", 6 | ":reviewer(team:ecosystem)" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Lock Threads' 3 | 4 | on: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | workflow_dispatch: 8 | 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | 13 | concurrency: 14 | group: ${{ github.workflow }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | action: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5 22 | with: 23 | issue-inactive-days: 0 24 | pr-inactive-days: 7 25 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI & Release 3 | 4 | # Workflow name based on selected inputs. Fallback to default Github naming when expression evaluates to empty string 5 | run-name: >- 6 | ${{ 7 | inputs.release && inputs.test && 'Build ➤ Test ➤ Publish to NPM' || 8 | inputs.release && !inputs.test && 'Build ➤ Skip Tests ➤ Publish to NPM' || 9 | github.event_name == 'workflow_dispatch' && inputs.test && 'Build ➤ Test' || 10 | github.event_name == 'workflow_dispatch' && !inputs.test && 'Build ➤ Skip Tests' || 11 | '' 12 | }} 13 | 14 | permissions: 15 | contents: read # for checkout 16 | 17 | on: 18 | # Build on pushes branches that have a PR (including drafts) 19 | pull_request: 20 | # Build on commits pushed to branches without a PR if it's in the allowlist 21 | push: 22 | branches: [main, studio-v2] 23 | # https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow 24 | workflow_dispatch: 25 | inputs: 26 | test: 27 | description: Run tests 28 | required: true 29 | default: true 30 | type: boolean 31 | release: 32 | description: Release new version 33 | required: true 34 | default: false 35 | type: boolean 36 | 37 | concurrency: 38 | # On PRs builds will cancel if new pushes happen before the CI completes, as it defines `github.head_ref` and gives it the name of the branch the PR wants to merge into 39 | # Otherwise `github.run_id` ensures that you can quickly merge a queue of PRs without causing tests to auto cancel on any of the commits pushed to main. 40 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 41 | cancel-in-progress: true 42 | 43 | jobs: 44 | build: 45 | runs-on: ubuntu-latest 46 | name: Lint & Build 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: actions/setup-node@v4 50 | with: 51 | cache: npm 52 | node-version: lts/* 53 | - run: npm ci 54 | # Linting can be skipped 55 | - run: npm run lint 56 | if: github.event.inputs.test != 'false' 57 | # But not the build script, as semantic-release will crash if this command fails so it makes sense to test it early 58 | - run: npm run prepublishOnly 59 | 60 | test: 61 | needs: build 62 | # The test matrix can be skipped, in case a new release needs to be fast-tracked and tests are already passing on main 63 | if: github.event.inputs.test != 'false' 64 | runs-on: ${{ matrix.os }} 65 | name: Node.js ${{ matrix.node }} / ${{ matrix.os }} 66 | strategy: 67 | # A test failing on windows doesn't mean it'll fail on macos. It's useful to let all tests run to its completion to get the full picture 68 | fail-fast: false 69 | matrix: 70 | # Run the testing suite on each major OS with the latest LTS release of Node.js 71 | os: [macos-latest, ubuntu-latest, windows-latest] 72 | node: [lts/*] 73 | # It makes sense to also test the oldest, and latest, versions of Node.js, on ubuntu-only since it's the fastest CI runner 74 | include: 75 | - os: ubuntu-latest 76 | # Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life 77 | node: lts/-2 78 | - os: ubuntu-latest 79 | # Test the actively developed version that will become the latest LTS release next October 80 | node: current 81 | steps: 82 | # It's only necessary to do this for windows, as mac and ubuntu are sane OS's that already use LF 83 | - name: Set git to use LF 84 | if: matrix.os == 'windows-latest' 85 | run: | 86 | git config --global core.autocrlf false 87 | git config --global core.eol lf 88 | - uses: actions/checkout@v4 89 | - uses: actions/setup-node@v4 90 | with: 91 | cache: npm 92 | node-version: ${{ matrix.node }} 93 | - run: npm install 94 | - run: npm test 95 | 96 | release: 97 | permissions: 98 | id-token: write # to enable use of OIDC for npm provenance 99 | needs: [build, test] 100 | # only run if opt-in during workflow_dispatch 101 | if: always() && github.event.inputs.release == 'true' && needs.build.result != 'failure' && needs.test.result != 'failure' && needs.test.result != 'cancelled' 102 | runs-on: ubuntu-latest 103 | name: Semantic release 104 | steps: 105 | - uses: actions/create-github-app-token@v2 106 | id: app-token 107 | with: 108 | app-id: ${{ secrets.ECOSPARK_APP_ID }} 109 | private-key: ${{ secrets.ECOSPARK_APP_PRIVATE_KEY }} 110 | - uses: actions/checkout@v4 111 | with: 112 | # Need to fetch entire commit history to 113 | # analyze every commit since last release 114 | fetch-depth: 0 115 | # Uses generated token to allow pushing commits back 116 | token: ${{ steps.app-token.outputs.token }} 117 | # Make sure the value of GITHUB_TOKEN will not be persisted in repo's config 118 | persist-credentials: false 119 | - uses: actions/setup-node@v4 120 | with: 121 | cache: npm 122 | node-version: lts/* 123 | - run: npm ci 124 | # Branches that will release new versions are defined in .releaserc.json 125 | - run: npx semantic-release 126 | # Don't allow interrupting the release step if the job is cancelled, as it can lead to an inconsistent state 127 | # e.g. git tags were pushed but it exited before `npm publish` 128 | if: always() 129 | env: 130 | NPM_CONFIG_PROVENANCE: true 131 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 132 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 133 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # macOS finder cache file 40 | .DS_Store 41 | 42 | # VS Code settings 43 | .vscode 44 | 45 | # IntelliJ 46 | .idea 47 | *.iml 48 | 49 | # Cache 50 | .cache 51 | 52 | # Yalc 53 | .yalc 54 | yalc.lock 55 | 56 | # npm package zips 57 | *.tgz 58 | 59 | # Compiled plugin 60 | lib 61 | 62 | docs 63 | tsconfig.tsbuildinfo 64 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib 2 | pnpm-lock.yaml 3 | yarn.lock 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": true, 5 | "singleQuote": true, 6 | "plugins": ["prettier-plugin-packagejson"] 7 | } 8 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/semantic-release-preset", 3 | "branches": ["main", { "name": "studio-v2", "channel": "studio-v2", "range": "0.x.x" }] 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 📓 Changelog 4 | 5 | All notable changes to this project will be documented in this file. See 6 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 7 | 8 | ## [3.0.4](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.3...v3.0.4) (2025-07-10) 9 | 10 | ### Bug Fixes 11 | 12 | - **deps:** allow studio v4 peer dep ranges ([f2ebd6a](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/f2ebd6a7591e5efeca68278eaaf75917c07967f2)) 13 | 14 | ## [3.0.3](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.2...v3.0.3) (2025-01-28) 15 | 16 | ### Bug Fixes 17 | 18 | - improve react 19 typings ([7b87db1](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/7b87db1637b26ce71cc591df121f86ce63099a66)) 19 | 20 | ## [3.0.2](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.1...v3.0.2) (2024-12-18) 21 | 22 | ### Bug Fixes 23 | 24 | - allow react 19 ([a0ce51f](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/a0ce51f68185730932aecb3cb59ec58e29490a16)) 25 | - **deps:** Update dependency @sanity/icons to v3 ([#154](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/154)) ([b9b8161](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/b9b8161dab78497134f5fd14ee930197611e8d17)) 26 | 27 | ## [3.0.1](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0...v3.0.1) (2024-04-11) 28 | 29 | ### Bug Fixes 30 | 31 | - bump sanity ui ([09c1563](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/09c156378cf6d78a1df9ad15970217be4813efbe)) 32 | - **deps:** bump non-major ([f5e54ed](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/f5e54ed40dd75e923a1f1686fd1ccfd180ac5714)) 33 | - **deps:** Update non-major ([#141](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/141)) ([6396bff](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/6396bff057558cc55b9bcd7dfde1c0cc7331500f)) 34 | 35 | ## [3.0.0](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v2.0.1...v3.0.0) (2024-04-08) 36 | 37 | ### ⚠ BREAKING CHANGES 38 | 39 | - require `styled-components` v6 (#138) 40 | 41 | ### Bug Fixes 42 | 43 | - require `styled-components` v6 ([#138](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/138)) ([044a74d](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/044a74da6cc5a7683f0e8a8eb48eae45bc424a8e)) 44 | 45 | ## [2.0.1](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v2.0.0...v2.0.1) (2024-03-12) 46 | 47 | ### Bug Fixes 48 | 49 | - improve UX of search input ([5ac85fd](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/5ac85fd06b5ce968c9ca1a09c0da283fd8d926b8)) 50 | 51 | ## [2.0.0](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v1.1.2...v2.0.0) (2024-03-12) 52 | 53 | ### ⚠ BREAKING CHANGES 54 | 55 | - update dependency @sanity/ui to v2 (#127) 56 | 57 | ### Bug Fixes 58 | 59 | - update dependency @sanity/ui to v2 ([#127](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/127)) ([a163e52](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/a163e52dac306ea5d0f67da08342bb7905f5bc32)) 60 | 61 | ## [1.1.2](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v1.1.1...v1.1.2) (2023-10-23) 62 | 63 | ### Bug Fixes 64 | 65 | - **deps:** update dependency styled-components to v6 ([13266e2](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/13266e22bd3a12941e80b9d1e62953dc6e9e3e2e)) 66 | 67 | ## [1.1.1](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v1.1.0...v1.1.1) (2023-09-03) 68 | 69 | ### Bug Fixes 70 | 71 | - **deps:** update non-major ([#112](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/112)) ([19cfa89](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/19cfa89aa931d552b34e23366a2888b74ac5698f)) 72 | 73 | ## [1.1.0](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v1.0.7...v1.1.0) (2023-05-23) 74 | 75 | ### Features 76 | 77 | - improve ESM support ([284d24e](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/284d24e4847e45ad7daaa932e4e7cbc961033995)) 78 | 79 | ## [1.0.7](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v1.0.6...v1.0.7) (2023-05-23) 80 | 81 | ### Bug Fixes 82 | 83 | - **deps:** update dependencies (non-major) ([#83](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/83)) ([f0d8e9a](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/f0d8e9a7501a74160c945b8aba260ab31562c2f4)) 84 | - don't steal focus from the search input ([6810a7c](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/6810a7c89609bd33dda20e728dde1b7cc87c8df7)) 85 | 86 | ## [1.0.6](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v1.0.5...v1.0.6) (2023-01-12) 87 | 88 | ### Bug Fixes 89 | 90 | - redraw unsplash logo to match the `@sanity/icons` grid ([5997b28](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/5997b2814af4c6371d3525b3210719d8293f7c7d)) 91 | 92 | ## [1.0.5](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v1.0.4...v1.0.5) (2023-01-12) 93 | 94 | ### Bug Fixes 95 | 96 | - **deps:** update dependency react-photo-album to v2 ([#68](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/68)) ([4748ce0](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/4748ce0507da4239377793c2b9621f3862cb3a5f)) 97 | 98 | ## [1.0.4](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v1.0.3...v1.0.4) (2023-01-12) 99 | 100 | ### Bug Fixes 101 | 102 | - adjust unsplash logo negative space with `viewBox` instead of CSS `transform: scale` ([d10e532](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/d10e5324e3b8eef5eebd8fe03326bb7f748c9cb3)) 103 | - better deduping of shared dependencies ([6b9eff7](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/6b9eff77baf95903541526ad4df8f36086d46754)) 104 | - replace `lodash/flatten` with native `Array.flat` ([90f8c07](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/90f8c07038e9f7f0f3ef566b826be9b84e3717c4)) 105 | 106 | ## [1.0.3](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v1.0.2...v1.0.3) (2023-01-04) 107 | 108 | ### Bug Fixes 109 | 110 | - **deps:** applied npx @sanity/plugin-kit inject ([a2631da](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/a2631daaa487f4bf51a78cff304373d76ebea186)) 111 | 112 | ## [1.0.2](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v1.0.1...v1.0.2) (2023-01-03) 113 | 114 | ### Bug Fixes 115 | 116 | - **docs:** formBuilder.image.assetSource should be form.image.assetSource ([36991e6](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/36991e69334c0d6e4c77efbe848df8e3cf1e7fef)) 117 | - vertically center unsplash logo ([dfa2612](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/dfa26120d994b98f861321a59e7466da28b70486)) 118 | 119 | ## [1.0.1](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v1.0.0...v1.0.1) (2022-11-25) 120 | 121 | ### Bug Fixes 122 | 123 | - **deps:** sanity ^3.0.0 (works with rc.3) ([2c4b6de](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/2c4b6de94c99d3a058b73b4e18f7caca2d4a81dc)) 124 | 125 | ## [1.0.0](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v0.2.4...v1.0.0) (2022-11-17) 126 | 127 | ### ⚠ BREAKING CHANGES 128 | 129 | - this version does not work in Sanity Studio v2 130 | - No longer works in Studio V3 131 | 132 | ### Features 133 | 134 | - initial Sanity Studio v3 release ([299e0ff](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/299e0ff6975e3c5c490927c08c089067a96d3f47)) 135 | - studio v3 version ([b96bc92](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/b96bc920ddab5f8b6c7fc14992b8e85fe8781822)) 136 | 137 | ### Bug Fixes 138 | 139 | - bump to `dev-preview.22` ([d891e62](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/d891e624ef4f6dd2ac435161b5bc828b0394338f)) 140 | - compiled for sanity 3.0.0-rc.0 ([7d38147](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/7d381479e1e126db165d914e6a3f0902f5ecc9b0)) 141 | - **deps:** dev-preview.21 ([3d27d95](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/3d27d951e93520872e4096d1a7dd04dca44c630d)) 142 | - **deps:** pin dependency @sanity/ui to 1.0.0-beta.32 ([#59](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/59)) ([ce459ce](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/ce459ce2a2b87fe4a827c0e3675492ee5fd4df9c)) 143 | - **deps:** pkg-utils & @sanity/plugin-kit ([09a49c0](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/09a49c021cf3573f92e895c99dd047f4fc565d4a)) 144 | - **deps:** sanity 3.0.0-dev-preview.17 ([fd846ce](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/fd846cec334a4a4e18a461e1cd2ae64271a36f54)) 145 | - **deps:** sanity/ui 0.38 ([8da5b91](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/8da5b9116316d856e766b39e8599775ed65b72c0)) 146 | - **deps:** update dependency rxjs to ^6.6.7 (v3) ([#21](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/21)) ([0d323ec](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/0d323ecdee947251ea187b91278a59fa01c9c273)) 147 | - **deps:** update sanity packages to v1 (v3) (major) ([#20](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/20)) ([2b1efd2](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/2b1efd2dd0de5e7ffd533678de2fe2a7321f79ac)) 148 | - replace `parcel` with `@sanity/pkg-utils` ([46988fd](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/46988fd04acf148b749b9b8d8e743405c10eff05)) 149 | - set apiVersion in useClient ([260e875](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/260e8754a9171177726463681ef555d3faab7aaa)) 150 | - styled-components is a peer dep ([d2051e2](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/d2051e27ac37ae8b3a5295b76a098aa3a897e0b1)) 151 | - update docs ([3776bbc](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/3776bbcb787f9d4e7457b9d39efe3ff05f62e794)) 152 | - use currentColor in Unsplash logo svg ([0c0b241](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/0c0b241e4ffdcdabe77a4dd807919c6fb1c8195c)) 153 | - use exact `@sanity/ui` version ([62a260c](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/62a260cab6fcfb15fb60986ffaf5b2210b31231f)) 154 | - use the same `@sanity/ui` as `dev-preview` ([156970d](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/156970d2f244a48e20a9e0712ed38a6dbafdb500)) 155 | 156 | ## [3.0.0-v3-studio.13](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.12...v3.0.0-v3-studio.13) (2022-11-04) 157 | 158 | ### Bug Fixes 159 | 160 | - **deps:** pkg-utils & @sanity/plugin-kit ([09a49c0](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/09a49c021cf3573f92e895c99dd047f4fc565d4a)) 161 | 162 | ## [3.0.0-v3-studio.12](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.11...v3.0.0-v3-studio.12) (2022-11-03) 163 | 164 | ### Bug Fixes 165 | 166 | - **deps:** pin dependency @sanity/ui to 1.0.0-beta.32 ([#59](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/59)) ([ce459ce](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/ce459ce2a2b87fe4a827c0e3675492ee5fd4df9c)) 167 | 168 | ## [3.0.0-v3-studio.11](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.10...v3.0.0-v3-studio.11) (2022-11-02) 169 | 170 | ### Bug Fixes 171 | 172 | - compiled for sanity 3.0.0-rc.0 ([7d38147](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/7d381479e1e126db165d914e6a3f0902f5ecc9b0)) 173 | 174 | ## [3.0.0-v3-studio.10](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.9...v3.0.0-v3-studio.10) (2022-10-25) 175 | 176 | ### Bug Fixes 177 | 178 | - use exact `@sanity/ui` version ([62a260c](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/62a260cab6fcfb15fb60986ffaf5b2210b31231f)) 179 | 180 | ## [3.0.0-v3-studio.9](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.8...v3.0.0-v3-studio.9) (2022-10-25) 181 | 182 | ### Bug Fixes 183 | 184 | - **deps:** update dependency rxjs to ^6.6.7 (v3) ([#21](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/21)) ([0d323ec](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/0d323ecdee947251ea187b91278a59fa01c9c273)) 185 | - replace `parcel` with `@sanity/pkg-utils` ([46988fd](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/46988fd04acf148b749b9b8d8e743405c10eff05)) 186 | 187 | ## [3.0.0-v3-studio.8](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.7...v3.0.0-v3-studio.8) (2022-10-24) 188 | 189 | ### Bug Fixes 190 | 191 | - bump to `dev-preview.22` ([d891e62](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/d891e624ef4f6dd2ac435161b5bc828b0394338f)) 192 | 193 | ## [3.0.0-v3-studio.7](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.6...v3.0.0-v3-studio.7) (2022-10-11) 194 | 195 | ### Bug Fixes 196 | 197 | - styled-components is a peer dep ([d2051e2](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/d2051e27ac37ae8b3a5295b76a098aa3a897e0b1)) 198 | - use the same `@sanity/ui` as `dev-preview` ([156970d](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/156970d2f244a48e20a9e0712ed38a6dbafdb500)) 199 | 200 | ## [3.0.0-v3-studio.6](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.5...v3.0.0-v3-studio.6) (2022-10-07) 201 | 202 | ### Bug Fixes 203 | 204 | - **deps:** dev-preview.21 ([3d27d95](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/3d27d951e93520872e4096d1a7dd04dca44c630d)) 205 | 206 | ## [3.0.0-v3-studio.5](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.4...v3.0.0-v3-studio.5) (2022-09-15) 207 | 208 | ### Bug Fixes 209 | 210 | - **deps:** sanity/ui 0.38 ([8da5b91](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/8da5b9116316d856e766b39e8599775ed65b72c0)) 211 | 212 | ## [3.0.0-v3-studio.4](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.3...v3.0.0-v3-studio.4) (2022-09-15) 213 | 214 | ### Bug Fixes 215 | 216 | - **deps:** sanity 3.0.0-dev-preview.17 ([fd846ce](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/fd846cec334a4a4e18a461e1cd2ae64271a36f54)) 217 | - set apiVersion in useClient ([260e875](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/260e8754a9171177726463681ef555d3faab7aaa)) 218 | 219 | # [3.0.0-v3-studio.3](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.2...v3.0.0-v3-studio.3) (2022-08-17) 220 | 221 | ### Bug Fixes 222 | 223 | - update docs ([3776bbc](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/3776bbcb787f9d4e7457b9d39efe3ff05f62e794)) 224 | 225 | # [3.0.0-v3-studio.2](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.1...v3.0.0-v3-studio.2) (2022-08-17) 226 | 227 | ### Bug Fixes 228 | 229 | - **deps:** update sanity packages to v1 (v3) (major) ([#20](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues/20)) ([2b1efd2](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/2b1efd2dd0de5e7ffd533678de2fe2a7321f79ac)) 230 | 231 | # [3.0.0-v3-studio.1](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v3.0.0-v3-studio.0...v3.0.0-v3-studio.1) (2022-08-17) 232 | 233 | ### Bug Fixes 234 | 235 | - use currentColor in Unsplash logo svg ([0c0b241](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/0c0b241e4ffdcdabe77a4dd807919c6fb1c8195c)) 236 | 237 | # [1.0.0-v3-studio.1](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v0.2.1...v1.0.0-v3-studio.1) (2022-08-17) 238 | 239 | - feat!: studio v3 version ([b96bc92](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/b96bc920ddab5f8b6c7fc14992b8e85fe8781822)) 240 | 241 | ### BREAKING CHANGES 242 | 243 | - No longer works in Studio V3 244 | 245 | ### [0.2.2](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/compare/v0.2.1...v0.2.2) (2022-05-05) 246 | 247 | ### Features 248 | 249 | - v3 upgrade ([e3d1bdb](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/commit/e3d1bdbb38a8764e0cf89f6e61feb9e06cf0379d)) 250 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Sanity.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sanity Asset Source Plugin: Unsplash 2 | 3 | > This is a **Sanity Studio v3** plugin. 4 | > For the v2 version, please refer to the [v2-branch](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/tree/studio-v2). 5 | 6 | ## What is it? 7 | 8 | Search for photos on Unsplash and add them to your project right inside Sanity Studio. 9 | 10 | ![Unsplash image selector](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/assets/81981/561c041f-bfe7-4c81-a93e-537d02da2664) 11 | 12 | ## Installation 13 | 14 | `npm install --save sanity-plugin-asset-source-unsplash` 15 | 16 | or 17 | 18 | `yarn add sanity-plugin-asset-source-unsplash` 19 | 20 | ## Usage 21 | 22 | Add it as a plugin in sanity.config.ts (or .js): 23 | 24 | ```js 25 | import { unsplashImageAsset } from 'sanity-plugin-asset-source-unsplash' 26 | 27 | export default defineConfig({ 28 | // ... 29 | plugins: [unsplashImageAsset()], 30 | }) 31 | ``` 32 | 33 | This will add [unsplashAssetSource](src/index.ts) to all image-fields in Sanity Studio 34 | 35 | ### Manually configure asset sources 36 | 37 | If you need to configure when Unsplash should be available as an asset source, filter it out as needed in 38 | `form.image.assetSources`: 39 | 40 | ```js 41 | import { unsplashImageAsset, unsplashAssetSource } from 'sanity-plugin-asset-source-unsplash' 42 | 43 | export default defineConfig({ 44 | // ... 45 | plugins: [unsplashImageAsset()], 46 | form: { 47 | image: { 48 | assetSources: (previousAssetSources, { schema }) => { 49 | if (schema.name === 'movie-image') { 50 | // remove unsplash from movie-image types 51 | return previousAssetSources.filter((assetSource) => assetSource !== unsplashAssetSource) 52 | } 53 | return previousAssetSources 54 | }, 55 | }, 56 | }, 57 | }) 58 | ``` 59 | 60 | ## Example Unsplash API Photo result 61 | 62 | ```json 63 | { 64 | "id": "1_CMoFsPfso", 65 | "created_at": "2016-08-27T05:14:20-04:00", 66 | "updated_at": "2019-11-07T00:01:26-05:00", 67 | "promoted_at": "2016-08-27T05:14:20-04:00", 68 | "width": 6016, 69 | "height": 4016, 70 | "color": "#170801", 71 | "description": "Minimal pencils on yellow", 72 | "alt_description": "two gray pencils on yellow surface", 73 | "urls": { 74 | "raw": "https://images.unsplash.com/photo-1472289065668-ce650ac443d2?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEwMDg3MX0", 75 | "full": "https://images.unsplash.com/photo-1472289065668-ce650ac443d2?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjEwMDg3MX0", 76 | "regular": "https://images.unsplash.com/photo-1472289065668-ce650ac443d2?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEwMDg3MX0", 77 | "small": "https://images.unsplash.com/photo-1472289065668-ce650ac443d2?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjEwMDg3MX0", 78 | "thumb": "https://images.unsplash.com/photo-1472289065668-ce650ac443d2?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjEwMDg3MX0" 79 | }, 80 | "links": { 81 | "self": "https://api.unsplash.com/photos/1_CMoFsPfso", 82 | "html": "https://unsplash.com/photos/1_CMoFsPfso", 83 | "download": "https://unsplash.com/photos/1_CMoFsPfso/download", 84 | "download_location": "https://api.unsplash.com/photos/1_CMoFsPfso/download" 85 | }, 86 | "categories": [], 87 | "likes": 4450, 88 | "liked_by_user": false, 89 | "current_user_collections": [], 90 | "user": { 91 | "id": "kA9qRJtrtA4", 92 | "updated_at": "2019-11-12T05:10:00-05:00", 93 | "username": "joannakosinska", 94 | "name": "Joanna Kosinska", 95 | "first_name": "Joanna", 96 | "last_name": "Kosinska", 97 | "twitter_username": "joannak.co.uk", 98 | "portfolio_url": null, 99 | "bio": null, 100 | "location": "Leeds", 101 | "links": { 102 | "self": "https://api.unsplash.com/users/joannakosinska", 103 | "html": "https://unsplash.com/@joannakosinska", 104 | "photos": "https://api.unsplash.com/users/joannakosinska/photos", 105 | "likes": "https://api.unsplash.com/users/joannakosinska/likes", 106 | "portfolio": "https://api.unsplash.com/users/joannakosinska/portfolio", 107 | "following": "https://api.unsplash.com/users/joannakosinska/following", 108 | "followers": "https://api.unsplash.com/users/joannakosinska/followers" 109 | }, 110 | "profile_image": { 111 | "small": "https://images.unsplash.com/profile-1477941848765-f577d5c83681?ixlib=rb-1.2.1&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32", 112 | "medium": "https://images.unsplash.com/profile-1477941848765-f577d5c83681?ixlib=rb-1.2.1&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64", 113 | "large": "https://images.unsplash.com/profile-1477941848765-f577d5c83681?ixlib=rb-1.2.1&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128" 114 | }, 115 | "instagram_username": "joannak.co.uk", 116 | "total_collections": 26, 117 | "total_likes": 534, 118 | "total_photos": 209, 119 | "accepted_tos": true 120 | } 121 | } 122 | ``` 123 | 124 | ## Example resulting asset document 125 | 126 | ```json 127 | { 128 | "ms": 7, 129 | "query": "*[_type == \"sanity.imageAsset\" \u0026\u0026 _id == \"image-a6904e5887baafcf72f686cfa3e98399fd3ff74a-2600x1548-jpg\"]", 130 | "result": [ 131 | { 132 | "_createdAt": "2019-11-14T09:01:47Z", 133 | "_id": "image-a6904e5887baafcf72f686cfa3e98399fd3ff74a-2600x1548-jpg", 134 | "_rev": "fDOLlTLScw9kMkHEI4HC9S", 135 | "_type": "sanity.imageAsset", 136 | "_updatedAt": "2019-11-14T09:01:47Z", 137 | "assetId": "a6904e5887baafcf72f686cfa3e98399fd3ff74a", 138 | "creditLine": "Qingbao Meng by Unsplash", 139 | "extension": "jpg", 140 | "metadata": { 141 | "_type": "sanity.imageMetadata", 142 | "dimensions": { 143 | "_type": "sanity.imageDimensions", 144 | "aspectRatio": 1.6795865633074936, 145 | "height": 1548, 146 | "width": 2600 147 | }, 148 | "hasAlpha": false, 149 | "isOpaque": true, 150 | "lqip": "data:image/jpeg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAMABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAUHBv/EACMQAAIBAgYCAwAAAAAAAAAAAAECAwAEBQYREiExFDJBUWH/xAAWAQEBAQAAAAAAAAAAAAAAAAACAwX/xAAcEQACAgIDAAAAAAAAAAAAAAAAAQIREiFBUaH/2gAMAwEAAhEDEQA/AMbh2arnaqvdggfR1ppc52eOMi3V5SOyTwKjOFsxLNvbj9pniU81t4whlcCT2GvdZ8080RUK5KHNm6F33SBlfTkCiptcXkwk03dD5op2uvRbP//Z", 151 | "palette": { 152 | "_type": "sanity.imagePalette", 153 | "darkMuted": { 154 | "_type": "sanity.imagePaletteSwatch", 155 | "background": "#435c39", 156 | "foreground": "#fff", 157 | "population": 6.83, 158 | "title": "#fff" 159 | }, 160 | "darkVibrant": { 161 | "_type": "sanity.imagePaletteSwatch", 162 | "background": "#698710", 163 | "foreground": "#fff", 164 | "population": 2.51, 165 | "title": "#fff" 166 | }, 167 | "dominant": { 168 | "_type": "sanity.imagePaletteSwatch", 169 | "background": "#728863", 170 | "foreground": "#fff", 171 | "population": 6.83, 172 | "title": "#fff" 173 | }, 174 | "lightMuted": { 175 | "_type": "sanity.imagePaletteSwatch", 176 | "background": "#bec6a9", 177 | "foreground": "#000", 178 | "population": 0.51, 179 | "title": "#fff" 180 | }, 181 | "lightVibrant": { 182 | "_type": "sanity.imagePaletteSwatch", 183 | "background": "#cad67e", 184 | "foreground": "#000", 185 | "population": 0.62, 186 | "title": "#000" 187 | }, 188 | "muted": { 189 | "_type": "sanity.imagePaletteSwatch", 190 | "background": "#728863", 191 | "foreground": "#fff", 192 | "population": 6.83, 193 | "title": "#fff" 194 | }, 195 | "vibrant": { 196 | "_type": "sanity.imagePaletteSwatch", 197 | "background": "#91b31a", 198 | "foreground": "#000", 199 | "population": 1.83, 200 | "title": "#fff" 201 | } 202 | } 203 | }, 204 | "mimeType": "image/jpeg", 205 | "originalFilename": "2bb1b89b-726e-4d1c-a148-7936f57a432f.jpeg", 206 | "path": "images/q2r21cu7/example/a6904e5887baafcf72f686cfa3e98399fd3ff74a-2600x1548.jpg", 207 | "sha1hash": "a6904e5887baafcf72f686cfa3e98399fd3ff74a", 208 | "size": 1101084, 209 | "source": { 210 | "id": "01_igFr7hd4", 211 | "name": "unsplash", 212 | "url": "https://unsplash.com/photos/01_igFr7hd4" 213 | }, 214 | "url": "https://cdn.sanity.io/images/q2r21cu7/example/a6904e5887baafcf72f686cfa3e98399fd3ff74a-2600x1548.jpg" 215 | } 216 | ] 217 | } 218 | ``` 219 | 220 | ## Futher reading 221 | 222 | - https://unsplash.com/documentation 223 | - https://www.sanity.io/docs/custom-asset-sources 224 | 225 | ## License 226 | 227 | MIT-licensed. See LICENSE. 228 | 229 | ## Develop & test 230 | 231 | This plugin uses [@sanity/plugin-kit](https://github.com/sanity-io/plugin-kit) 232 | with default configuration for build & watch scripts. 233 | 234 | See [Testing a plugin in Sanity Studio](https://github.com/sanity-io/plugin-kit#testing-a-plugin-in-sanity-studio) 235 | on how to run this plugin with hotreload in the studio. 236 | 237 | ### Release new version 238 | 239 | Run ["CI & Release" workflow](https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/actions/workflows/main.yml). 240 | Make sure to select the main branch and check "Release new version". 241 | 242 | Semantic release will only release on configured branches, so it is safe to run release on any branch. 243 | -------------------------------------------------------------------------------- /assets/unsplash-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/sanity-plugin-asset-source-unsplash/593c0a06cb7acadf491ffbb64b3a3d3f543df9d0/assets/unsplash-selector.png -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /lint-staged.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '**/*.{js,jsx}': ['eslint'], 3 | '**/*.{ts,tsx}': ['eslint', () => 'tsc --noEmit'], 4 | } 5 | -------------------------------------------------------------------------------- /package.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@sanity/pkg-utils' 2 | 3 | export default defineConfig({ 4 | dist: 'lib', 5 | tsconfig: 'tsconfig.lib.json', 6 | 7 | extract: { 8 | rules: { 9 | 'ae-internal-missing-underscore': 'off', 10 | }, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-plugin-asset-source-unsplash", 3 | "version": "3.0.4", 4 | "description": "Use images from Unsplash.com in your Sanity Studio", 5 | "keywords": [ 6 | "sanity", 7 | "plugin", 8 | "images", 9 | "unsplash", 10 | "assets", 11 | "source" 12 | ], 13 | "homepage": "https://github.com/sanity-io/sanity-plugin-asset-source-unsplash#readme", 14 | "bugs": { 15 | "url": "https://github.com/sanity-io/sanity-plugin-asset-source-unsplash/issues" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:sanity-io/sanity-plugin-asset-source-unsplash.git" 20 | }, 21 | "license": "MIT", 22 | "author": "Sanity.io ", 23 | "sideEffects": false, 24 | "type": "module", 25 | "exports": { 26 | ".": { 27 | "source": "./src/index.ts", 28 | "import": "./lib/index.js", 29 | "require": "./lib/index.cjs", 30 | "default": "./lib/index.js" 31 | }, 32 | "./package.json": "./package.json" 33 | }, 34 | "main": "./lib/index.cjs", 35 | "module": "./lib/index.js", 36 | "types": "./lib/index.d.ts", 37 | "files": [ 38 | "src", 39 | "lib", 40 | "sanity.json", 41 | "v2-incompatible.js" 42 | ], 43 | "scripts": { 44 | "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean", 45 | "format": "prettier --write --cache --ignore-unknown .", 46 | "link-watch": "plugin-kit link-watch", 47 | "lint": "eslint .", 48 | "prepare": "husky install || true", 49 | "prepublishOnly": "npm run build", 50 | "test": "npm run lint && npm run type-check && npm run build", 51 | "type-check": "tsc --noEmit", 52 | "watch": "pkg-utils watch --strict" 53 | }, 54 | "browserslist": "extends @sanity/browserslist-config", 55 | "dependencies": { 56 | "@sanity/icons": "^3.5.7", 57 | "@sanity/incompatible-plugin": "^1.0.5", 58 | "@sanity/ui": "^2.11.6", 59 | "react-infinite-scroll-component": "6.1.0", 60 | "react-photo-album": "2.4.1", 61 | "rxjs": "^7.8.1" 62 | }, 63 | "devDependencies": { 64 | "@commitlint/cli": "^19.6.1", 65 | "@commitlint/config-conventional": "^19.6.0", 66 | "@sanity/pkg-utils": "^7.0.3", 67 | "@sanity/plugin-kit": "4.0.19", 68 | "@sanity/semantic-release-preset": "^4.1.8", 69 | "@types/node": "^18.18.6", 70 | "@types/react": "^19.0.8", 71 | "@types/react-dom": "^19.0.3", 72 | "@typescript-eslint/eslint-plugin": "^8.22.0", 73 | "@typescript-eslint/parser": "^8.22.0", 74 | "eslint": "^8.57.1", 75 | "eslint-config-prettier": "^9.1.0", 76 | "eslint-config-sanity": "^7.1.4", 77 | "eslint-plugin-import": "^2.31.0", 78 | "eslint-plugin-prettier": "^5.2.3", 79 | "eslint-plugin-react": "^7.37.4", 80 | "eslint-plugin-react-hooks": "^5.1.0", 81 | "eslint-plugin-simple-import-sort": "^12.1.1", 82 | "husky": "^8.0.3", 83 | "lint-staged": "^15.0.2", 84 | "prettier": "^3.4.2", 85 | "prettier-plugin-packagejson": "^2.5.8", 86 | "react": "^19.0.0", 87 | "react-dom": "^19.0.0", 88 | "sanity": "^3.71.2", 89 | "semantic-release": "^23.1.1", 90 | "styled-components": "^6.1.14", 91 | "typescript": "5.7.3" 92 | }, 93 | "peerDependencies": { 94 | "react": "^18 || ^19", 95 | "sanity": "^3 || ^4.0.0-0", 96 | "styled-components": "^6.1" 97 | }, 98 | "engines": { 99 | "node": ">=18" 100 | }, 101 | "publishConfig": { 102 | "access": "public" 103 | }, 104 | "sanityExchangeUrl": "https://www.sanity.io/plugins/sanity-plugin-asset-source-unsplash" 105 | } 106 | -------------------------------------------------------------------------------- /sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/sanity-root", 5 | "path": "./v2-incompatible.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | export default function UnsplashIcon() { 2 | return ( 3 | 4 | 5 | 6 | 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Photo.styled.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from '@sanity/ui' 2 | import { styled } from 'styled-components' 3 | 4 | export const Root = styled.div` 5 | overflow: hidden; 6 | background-origin: content-box; 7 | background-repeat: no-repeat; 8 | background-clip: border-box; 9 | background-size: cover; 10 | position: relative; 11 | outline: none !important; 12 | border: ${({ theme }) => `1px solid ${theme.sanity.color.card.enabled.border}`}; 13 | box-sizing: content-box; 14 | -webkit-user-drag: none; 15 | 16 | &:hover { 17 | opacity: 0.85; 18 | } 19 | 20 | &:focus, 21 | &:active { 22 | border: 1px solid var(--input-border-color-focus); 23 | box-shadow: inset 0 0 0 3px var(--input-border-color-focus); 24 | } 25 | ` 26 | 27 | export const CreditLineLink = styled.a` 28 | text-decoration: none; 29 | cursor: pointer; 30 | ` 31 | 32 | export const CreditLine = styled(Card)` 33 | ${({ theme }) => ` 34 | --creditline-fg: ${theme.sanity.color.card.enabled.fg}; 35 | --creditline-bg: ${theme.sanity.color.card.enabled.bg}; 36 | `}; 37 | -webkit-user-drag: none; 38 | position: absolute; 39 | background-color: var(--creditline-bg); 40 | bottom: 0; 41 | 42 | [data-ui='Text'] { 43 | color: var(--creditline-fg); 44 | } 45 | ` 46 | -------------------------------------------------------------------------------- /src/components/Photo.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from '@sanity/ui' 2 | import { useCallback, useEffect } from 'react' 3 | 4 | import type { UnsplashPhoto } from '../types' 5 | import { CreditLine, CreditLineLink, Root } from './Photo.styled' 6 | 7 | type Props = { 8 | data: UnsplashPhoto 9 | width: number 10 | height: number 11 | onClick: (photo: UnsplashPhoto) => void 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | onKeyDown: (event: any) => void 14 | active: boolean 15 | onFocus: (photo: UnsplashPhoto) => void 16 | } 17 | 18 | const UTM_SOURCE = 'sanity-plugin-asset-source-unsplash' 19 | 20 | export default function Photo(props: Props) { 21 | const { onClick, data, onKeyDown, onFocus, active, width, height } = props 22 | 23 | const handleClick = useCallback(() => { 24 | onClick(data) 25 | }, [onClick, data]) 26 | 27 | const handleCreditLineClicked = useCallback( 28 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 29 | (event: any) => { 30 | event.stopPropagation() 31 | const url = `${data.links.html}?utm_source=${encodeURIComponent( 32 | UTM_SOURCE, 33 | )}&utm_medium=referral` 34 | window.open(url, data.id, 'noreferrer,noopener') 35 | }, 36 | [data], 37 | ) 38 | 39 | const handleKeyDown = useCallback( 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | (event: any) => { 42 | onKeyDown(event) 43 | if (event.keyCode === 13) { 44 | onClick(data) 45 | } 46 | }, 47 | [onKeyDown, data, onClick], 48 | ) 49 | 50 | const handleMouseDown = useCallback(() => { 51 | onFocus(data) 52 | }, [onFocus, data]) 53 | 54 | useEffect(() => { 55 | if (active) { 56 | onFocus(data) 57 | } 58 | }, [active, data, onFocus]) 59 | 60 | const src = data.urls.small 61 | const userName = data.user.name 62 | 63 | return ( 64 | 76 | 77 | 78 | 79 | By @{data.user.username} 80 | 81 | 82 | 83 | 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /src/components/UnsplashAssetSource.styled.tsx: -------------------------------------------------------------------------------- 1 | import { TextInput } from '@sanity/ui' 2 | import { styled } from 'styled-components' 3 | 4 | export const SearchInput = styled(TextInput)` 5 | position: sticky; 6 | top: 0; 7 | z-index: 1; 8 | ` 9 | 10 | export const Scroller = styled.div` 11 | overflow-y: auto; 12 | max-height: 80vh; 13 | ` 14 | -------------------------------------------------------------------------------- /src/components/UnsplashAssetSource.tsx: -------------------------------------------------------------------------------- 1 | import { SearchIcon } from '@sanity/icons' 2 | import { Dialog, Flex, Spinner, Stack, Text } from '@sanity/ui' 3 | import React from 'react' 4 | import InfiniteScroll from 'react-infinite-scroll-component' 5 | import PhotoAlbum from 'react-photo-album' 6 | import { BehaviorSubject, type Subscription } from 'rxjs' 7 | import { 8 | type AssetFromSource, 9 | type AssetSourceComponentProps, 10 | type SanityClient, 11 | useClient, 12 | } from 'sanity' 13 | 14 | import { fetchDownloadUrl, search } from '../datastores/unsplash' 15 | import type { UnsplashPhoto } from '../types' 16 | import Photo from './Photo' 17 | import { SearchInput } from './UnsplashAssetSource.styled' 18 | 19 | type State = { 20 | query: string 21 | searchResults: UnsplashPhoto[][] 22 | page: number 23 | isLoading: boolean 24 | cursor: number 25 | } 26 | 27 | const RESULTS_PER_PAGE = 42 28 | const PHOTO_SPACING = 2 29 | const PHOTO_PADDING = 1 // offset the 1px border width 30 | 31 | export default function UnsplashAssetSource(props: AssetSourceComponentProps) { 32 | const client = useClient({ apiVersion: '2022-09-01' }) 33 | return 34 | } 35 | 36 | class UnsplashAssetSourceInternal extends React.Component< 37 | AssetSourceComponentProps & { client: SanityClient }, 38 | State 39 | > { 40 | static defaultProps = { 41 | selectedAssets: undefined, 42 | } 43 | 44 | override state = { 45 | cursor: 0, 46 | query: '', 47 | page: 1, 48 | searchResults: [[]], 49 | isLoading: true, 50 | } 51 | 52 | searchSubscription: Subscription | null = null 53 | 54 | searchSubject$ = new BehaviorSubject('') 55 | pageSubject$ = new BehaviorSubject(1) 56 | 57 | override componentDidMount() { 58 | this.searchSubscription = search( 59 | this.props.client, 60 | this.searchSubject$, 61 | this.pageSubject$, 62 | RESULTS_PER_PAGE, 63 | ).subscribe({ 64 | next: (results: UnsplashPhoto[]) => { 65 | this.setState((prev) => ({ 66 | searchResults: [...prev.searchResults, results], 67 | isLoading: false, 68 | })) 69 | }, 70 | }) 71 | } 72 | 73 | override componentWillUnmount() { 74 | if (this.searchSubscription) { 75 | this.searchSubscription.unsubscribe() 76 | } 77 | } 78 | 79 | handleSelect = (photo: UnsplashPhoto) => { 80 | this.setState({ isLoading: true }) 81 | return fetchDownloadUrl(this.props.client, photo).then((downloadUrl) => { 82 | const description = photo.description || undefined 83 | const asset: AssetFromSource = { 84 | kind: 'url', 85 | value: downloadUrl, 86 | assetDocumentProps: { 87 | _type: 'sanity.imageAsset', 88 | source: { 89 | name: 'unsplash', 90 | id: photo.id, 91 | url: photo.links.html, 92 | }, 93 | description, 94 | creditLine: `${photo.user.name} by Unsplash`, 95 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 96 | } as any, 97 | } 98 | this.props.onSelect([asset]) 99 | }) 100 | } 101 | 102 | handleClose = () => { 103 | this.props.onClose() 104 | } 105 | 106 | handleSearchTermChanged = (event: React.ChangeEvent) => { 107 | const query = event.currentTarget.value 108 | this.setState({ query, page: 1, searchResults: [[]], isLoading: true, cursor: 0 }) 109 | this.pageSubject$.next(1) 110 | this.searchSubject$.next(query) 111 | } 112 | 113 | handleSearchTermCleared = () => { 114 | this.setState({ query: '', page: 1, searchResults: [[]], isLoading: true, cursor: 0 }) 115 | this.pageSubject$.next(1) 116 | this.searchSubject$.next('') 117 | } 118 | 119 | handleScollerLoadMore = () => { 120 | // eslint-disable-next-line react/no-access-state-in-setstate 121 | const nextPage = this.state.page + 1 122 | this.setState({ page: nextPage, isLoading: true }) 123 | this.pageSubject$.next(nextPage) 124 | this.searchSubject$.next(this.state.query) 125 | } 126 | 127 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 128 | handleKeyDown = (event: any) => { 129 | const { cursor } = this.state 130 | if ((event.keyCode === 38 || event.keyCode === 37) && cursor > 0) { 131 | this.setState((prevState) => ({ 132 | cursor: prevState.cursor - 1, 133 | })) 134 | } else if ( 135 | (event.keyCode === 40 || event.keyCode === 39) && 136 | cursor < this.getPhotos().length - 1 137 | ) { 138 | this.setState((prevState) => ({ 139 | cursor: prevState.cursor + 1, 140 | })) 141 | } 142 | } 143 | 144 | getPhotos() { 145 | return this.state.searchResults.flat() 146 | } 147 | 148 | updateCursor = (photo: UnsplashPhoto) => { 149 | const index = this.getPhotos().findIndex((result: UnsplashPhoto) => result.id === photo.id) 150 | this.setState({ cursor: index }) 151 | } 152 | 153 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 154 | renderImage = (props: any) => { 155 | const { photo, layout } = props 156 | const active = 157 | this.getPhotos().findIndex((result: UnsplashPhoto) => result.id === photo.data.id) === 158 | this.state.cursor || false 159 | return ( 160 | 169 | ) 170 | } 171 | 172 | override render() { 173 | const { query, searchResults, isLoading } = this.state 174 | 175 | return ( 176 | 184 | 185 | 0} 187 | icon={SearchIcon} 188 | onChange={this.handleSearchTermChanged} 189 | onClear={this.handleSearchTermCleared} 190 | placeholder="Search by topics or colors" 191 | value={query} 192 | /> 193 | {!isLoading && this.getPhotos().length === 0 && ( 194 | 195 | No results found 196 | 197 | )} 198 | 207 | 208 | 209 | } 210 | endMessage={ 211 | 212 | No more results 213 | 214 | } 215 | > 216 | {searchResults 217 | .filter((photos) => photos.length > 0) 218 | .map((photos: UnsplashPhoto[], index) => ( 219 | { 226 | if (width < 300) return 150 227 | if (width < 600) return 200 228 | return 300 229 | }} 230 | photos={photos.map((photo: UnsplashPhoto) => ({ 231 | src: photo.urls.small, 232 | width: photo.width, 233 | height: photo.height, 234 | key: photo.id, 235 | data: photo, 236 | }))} 237 | renderPhoto={this.renderImage} 238 | componentsProps={{ 239 | containerProps: { style: { marginBottom: `${PHOTO_SPACING}px` } }, 240 | }} 241 | /> 242 | ))} 243 | 244 | 245 | 246 | ) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/datastores/unsplash.ts: -------------------------------------------------------------------------------- 1 | import { type BehaviorSubject, concat, defer, type Observable } from 'rxjs' 2 | import { debounceTime, distinctUntilChanged, map, switchMap, withLatestFrom } from 'rxjs/operators' 3 | import type { SanityClient } from 'sanity' 4 | 5 | import type { UnsplashPhoto } from '../types' 6 | 7 | type SearchSubject = BehaviorSubject 8 | type PageSubject = BehaviorSubject 9 | 10 | const fetchSearch = ( 11 | client: SanityClient, 12 | query: string, 13 | page: number, 14 | perPage: number, 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | ): Observable => 17 | defer( 18 | () => 19 | client.observable.request({ 20 | url: `/addons/unsplash/search/photos?query=${encodeURIComponent( 21 | query, 22 | )}&page=${page}&per_page=${perPage}`, 23 | withCredentials: true, 24 | method: 'GET', 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | }) as any, 27 | ) 28 | 29 | const fetchList = ( 30 | client: SanityClient, 31 | type: string, 32 | page: number, 33 | perPage: number, 34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | ): Observable => 36 | defer( 37 | () => 38 | client.observable.request({ 39 | url: `/addons/unsplash/photos?order_by=${type}&page=${page}&per_page=${perPage}`, 40 | withCredentials: true, 41 | method: 'GET', 42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 43 | }) as any, 44 | ) 45 | 46 | export function fetchDownloadUrl(client: SanityClient, photo: UnsplashPhoto): Promise { 47 | const downloadUrl = photo.links.download_location.replace( 48 | 'https://api.unsplash.com', 49 | '/addons/unsplash', 50 | ) 51 | return client 52 | .request({ 53 | url: downloadUrl, 54 | withCredentials: true, 55 | method: 'GET', 56 | }) 57 | .then((result: { url: string }) => { 58 | return result.url 59 | }) 60 | } 61 | 62 | export const search = ( 63 | client: SanityClient, 64 | query: SearchSubject, 65 | page: PageSubject, 66 | resultsPerPage: number, 67 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 68 | ): Observable => { 69 | return concat( 70 | query.pipe( 71 | withLatestFrom(page), 72 | debounceTime(500), 73 | distinctUntilChanged(), 74 | switchMap(([q, p]) => { 75 | if (q) { 76 | return fetchSearch(client, q, p, resultsPerPage).pipe( 77 | distinctUntilChanged(), 78 | map((result) => result.results), 79 | ) 80 | } 81 | return fetchList(client, 'popular', p, resultsPerPage) 82 | }), 83 | ), 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { type AssetSource, definePlugin } from 'sanity' 2 | 3 | import Icon from './components/Icon' 4 | import Unsplash from './components/UnsplashAssetSource' 5 | 6 | export type { Asset, AssetDocument, UnsplashPhoto } from './types' 7 | 8 | /** 9 | * @public 10 | */ 11 | export const unsplashAssetSource: AssetSource = { 12 | name: 'unsplash', 13 | title: 'Unsplash', 14 | component: Unsplash, 15 | icon: Icon, 16 | } 17 | 18 | /** 19 | * @public 20 | */ 21 | export const unsplashImageAsset = definePlugin({ 22 | name: 'asset-source-unsplash-plugin', 23 | 24 | form: { 25 | image: { 26 | assetSources: (prev) => { 27 | return [...prev, unsplashAssetSource] 28 | }, 29 | }, 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @public 3 | */ 4 | export interface Asset { 5 | kind: 'url' | 'base64' | 'file' | 'assetDocumentId' 6 | value: string | File 7 | assetDocumentProps?: { 8 | originalFileName?: string 9 | label?: string 10 | title?: string 11 | description?: string 12 | source?: { 13 | id: string 14 | name: string 15 | url?: string 16 | } 17 | creditLine?: string 18 | } 19 | } 20 | 21 | /** 22 | * @public 23 | */ 24 | export interface AssetDocument { 25 | _id: string 26 | label?: string 27 | title?: string 28 | description?: string 29 | source?: { 30 | id: string 31 | name: string 32 | url?: string 33 | } 34 | creditLine?: string 35 | originalFilename?: string 36 | } 37 | 38 | /** 39 | * @public 40 | */ 41 | export interface UnsplashPhoto { 42 | id: string 43 | width: number 44 | height: number 45 | description?: string 46 | alt_description?: string 47 | urls: { 48 | full: string 49 | small: string 50 | } 51 | user: { 52 | name: string 53 | username: string 54 | links: { 55 | html: string 56 | } 57 | } 58 | links: { 59 | html: string 60 | self: string 61 | download: string 62 | download_location: string 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.settings", 3 | "include": ["./src", "./package.config.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.settings", 3 | "include": ["./src"], 4 | "exclude": [ 5 | "./src/**/__fixtures__", 6 | "./src/**/__mocks__", 7 | "./src/**/*.test.ts", 8 | "./src/**/*.test.tsx" 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/pkg-utils/tsconfig/strictest.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "lib", 6 | "noPropertyAccessFromIndexSignature": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /v2-incompatible.js: -------------------------------------------------------------------------------- 1 | const { showIncompatiblePluginDialog } = require('@sanity/incompatible-plugin') 2 | const { name, version, sanityExchangeUrl } = require('./package.json') 3 | 4 | export default showIncompatiblePluginDialog({ 5 | name: name, 6 | versions: { 7 | v3: version, 8 | v2: '^0.2.1', 9 | }, 10 | sanityExchangeUrl, 11 | }) 12 | --------------------------------------------------------------------------------