diff --git a/.claude/skills/doc-feedback.md b/.claude/skills/doc-feedback.md new file mode 100644 index 000000000..17d3de3d4 --- /dev/null +++ b/.claude/skills/doc-feedback.md @@ -0,0 +1,42 @@ +--- +name: reviewing-documentation +description: Reviews documentation for quality, consistency, and style guide adherence. Use when reviewing changed docs or before publishing new content. +--- + +# Documentation Feedback + +## Workflow + +1. **Get files to review** + ```bash + git diff --name-only HEAD -- '*.mdx' + git diff --name-only master...HEAD -- '*.mdx' + ``` + If `$ARGUMENTS` provided, review that path instead. + +2. **Run linter first** + ```bash + node scripts/lint-mdx.js $ARGUMENTS + ``` + +3. **Review against style guide** — See [content-instructions.md](../../content-instructions.md) + +4. **Provide feedback** per file: + - What's working well + - Specific suggestions with line references + - Linter issues (if any) + +5. **Offer to fix** issues if requested + +## Review checklist + +``` +Review Progress: +- [ ] Terminology consistent +- [ ] Code examples complete and runnable +- [ ] No placeholder values (foo, bar, example.com) +- [ ] Headings descriptive and keyword-rich +- [ ] Content scannable (headings, lists, white space) +- [ ] Active voice, second person +- [ ] Troubleshooting included where appropriate +``` diff --git a/.claude/skills/lint.md b/.claude/skills/lint.md new file mode 100644 index 000000000..5a2076e21 --- /dev/null +++ b/.claude/skills/lint.md @@ -0,0 +1,25 @@ +--- +name: linting-mdx +description: Runs deterministic MDX linter and helps fix formatting, structure, and Mintlify component issues. Use when checking documentation quality or before committing changes. +--- + +# Lint MDX + +Run the linter: + +```bash +node scripts/lint-mdx.js $ARGUMENTS +``` + +Arguments: (none) = changed files, `all` = everything, or specify a path. + +## Workflow + +1. Run linter, present results +2. If errors found, offer to fix them +3. Prioritize errors over warnings + +## References + +- [mintlify-reference.md](../../mintlify-reference.md) — component syntax +- [content-instructions.md](../../content-instructions.md) — writing guidelines diff --git a/claude.md b/claude.md new file mode 100644 index 000000000..1b459c5c9 --- /dev/null +++ b/claude.md @@ -0,0 +1,64 @@ +# Base Documentation + +Technical documentation for Base (Ethereum L2). Built with Mintlify. + +## Commands + +| Command | Description | +|---------|-------------| +| `mintlify dev` | Local dev server | +| `/lint` | Lint MDX files and fix issues | +| `/doc-feedback` | Review content quality | + +## Structure + +``` +docs/ +├── get-started/ # Intro, quickstarts +├── base-chain/ # Network, nodes, tools +├── base-account/ # Smart Wallet SDK +├── base-app/ # Agent development +├── mini-apps/ # MiniKit guides +├── onchainkit/ # React components (versioned) +├── cookbook/ # Tutorials +├── learn/ # Solidity, Ethereum basics +├── images/ # Assets by topic +├── snippets/ # Reusable MDX components +└── docs.json # Navigation config +``` + +## Content Rules + +**Frontmatter** (required): +```yaml +--- +title: "Keyword-rich title" +description: "Value description" +--- +``` + +**Writing**: American English, sentence case headings, second person ("you"), active voice. + +**Code blocks**: Always specify language. Add filename or title. Use `highlight={}` for emphasis. + +**Components**: See [mintlify-reference.md](mintlify-reference.md) for syntax. + +**Images**: Wrap in ``, include `alt` attribute. + +## Navigation + +Edit `docs.json` to add/remove pages. Add redirects when removing pages. + +## References + +| File | Purpose | +|------|---------| +| [content-instructions.md](content-instructions.md) | Writing guidelines | +| [mintlify-reference.md](mintlify-reference.md) | Component syntax | +| [scripts/README.md](scripts/README.md) | Linter usage | + +## Before Committing + +1. Run `/lint` and fix errors +2. Add redirects for removed pages +3. Verify links work diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md deleted file mode 100644 index f95651952..000000000 --- a/docs/CLAUDE.md +++ /dev/null @@ -1,98 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Repository Overview - -This is the **Base Documentation** repository, a comprehensive documentation site for Base (Ethereum Layer 2 blockchain) built with **Mintlify**. The repository covers blockchain development from basics to advanced topics including smart contracts, onchain apps, and Base ecosystem tools. - -## Key Commands - -### Local Development -```bash -# Install Mintlify CLI globally (required for local development) -npm i -g mintlify - -# Run local development server -mintlify dev - -# Reinstall dependencies if mintlify dev fails -mintlify install -``` - -### Storybook Development -```bash -cd storybook -npm install -npm run storybook # Runs on port 6006 -npm run build-storybook -``` - -## Architecture and Structure - -### Documentation Organization -- **Tab-based navigation** with 7 main sections in `docs.json`: - - Get Started (introduction, quickstarts) - - Base Chain (network info, node operations) - - Smart Wallet (account abstraction) - - OnchainKit (React components) - - Wallet App (MiniKit development) - - Cookbook (use-case guides) - - Learn (educational content) - -### File Structure -- **Main content**: All documentation files are in `/docs` directory as `.mdx` files -- **Configuration**: `docs.json` defines navigation structure and site settings -- **Images**: Organized in `/images` with subdirectories by topic -- **Custom components**: React components available in `/snippets` -- **Styling**: `custom.css` for site-wide styles, `iframe-theme.js` for theme handling - -### Content Types -- **Educational tutorials** (Learn section with Solidity, Ethereum basics) -- **Practical guides** (Cookbook with real-world implementation examples) -- **API documentation** (OnchainKit components and utilities) -- **Tool documentation** (Smart Wallet, development frameworks) - -## Development Workflow -- Use sub-agents to help complete tasks -- Leverage the Github CLI tool to create commits for changes and to read the history of changes -- If you create scratch files for problem solving, remove them when you are done with them. - -### Content Editing -- Edit `.mdx` files directly in `/docs` directory (NOT `/_pages`) -- Use Mintlify syntax for enhanced documentation features -- Changes are automatically deployed via Mintlify GitHub App when pushed to default branch - -### Component Development -- Storybook setup in `/storybook` directory for UI component development -- Components integrated into documentation via iframe embedding -- Chromatic integration for visual testing and regression detection - -### Important Rules -- **Never edit files in `/_pages` directory** - all documentation files are in `/docs` -- Use Mintlify CLI for local development and testing -- Follow existing navigation structure defined in `docs.json` -- Images should be placed in appropriate `/images` subdirectories - -## Special Features - -### AI Integration -- Contextual AI options (ChatGPT, Claude) available in documentation interface -- Large `llms-full.txt` file (11,496 lines) provides comprehensive AI context -- AI prompting guides integrated throughout documentation - -### Interactive Components -- Storybook components embedded via iframes for interactive examples -- Dark/light mode support with custom theme handling -- Custom React components available for enhanced documentation experiences - -### Cross-referencing -- Extensive internal linking between related topics -- Progressive learning paths from beginner to advanced -- Use-case driven organization alongside technical reference material - -## Deployment - -- **Automatic deployment** via Mintlify GitHub App when changes are pushed to default branch -- **Storybook deployment** to Chromatic for component library hosting -- No manual deployment steps required for documentation updates \ No newline at end of file diff --git a/docs/base-chain/network-information/block-building.mdx b/docs/base-chain/network-information/block-building.mdx index df98d467e..99ec7866b 100644 --- a/docs/base-chain/network-information/block-building.mdx +++ b/docs/base-chain/network-information/block-building.mdx @@ -24,11 +24,11 @@ See the [Configuration Changelog](/base-chain/network-information/configuration- Currently, blocks are built using [op-rbuilder](https://github.com/flashbots/op-rbuilder) and priority fee auctions occur every 200ms. There are two changes from the vanilla ordering to be aware of: -##### Timing +#### Timing Flashblocks are built every 200ms, each ordering a portion of the block. Unlike the current system where later-arriving transactions with higher priority fees can be placed at the top of the block, Flashblocks creates a time-based constraint. Once a Flashblock is built and broadcast, its transaction ordering is locked even if a transaction with a higher priority fee arrives later, it cannot be included in earlier, already built Flashblocks. -##### High Gas Limits +#### High Gas Limits If your app creates transactions with large gas limits, we recommend monitoring to detect any changes in inclusion latency. Transactions with gas limits over 1/10 of the current block gas limit (currently 14 million gas), face additional constraints: diff --git a/docs/base-chain/network-information/transaction-finality.mdx b/docs/base-chain/network-information/transaction-finality.mdx index eed553521..01ae09508 100644 --- a/docs/base-chain/network-information/transaction-finality.mdx +++ b/docs/base-chain/network-information/transaction-finality.mdx @@ -19,7 +19,9 @@ This describes finality for transactions on Base except withdrawal transactions For transactions on Base, finality is not a single time to wait for. Instead, there are 4 stages in time that each provide increasing security guarantees. + ![Diagram of transaction finality stages on Base](/images/transaction-finality/base-tx-finality.jpg) + diff --git a/docs/learn/hardhat/etherscan/etherscan-sbs.mdx b/docs/learn/hardhat/etherscan/etherscan-sbs.mdx index a8517bfcb..399f2a443 100644 --- a/docs/learn/hardhat/etherscan/etherscan-sbs.mdx +++ b/docs/learn/hardhat/etherscan/etherscan-sbs.mdx @@ -24,7 +24,9 @@ By the end of this lesson, you should be able to: [Etherscan](https://etherscan.io) is a popular Blockchain explorer that works for several different networks. In it, you can explore the state and activity of a particular network. + ![Etherscan](/images/learn/etherscan/etherscan-user-interface.png) + You can explore: @@ -35,7 +37,9 @@ You can explore: For instance, the following shows the details of a Block: + ![Block](/images/learn/etherscan/blocks.png) + Where you see information such as: @@ -56,7 +60,9 @@ One of the things you can do with Etherscan is interact with already-deployed co For example, if you want to read information from a famous contract such as [BAYC](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d), you can simply go to Etherscan and explore the contract: + ![BAYC](/images/learn/etherscan/bayc.png) + You are able to see information such as: @@ -68,19 +74,25 @@ You are able to see information such as: In the **Contract** tab, you can see the full source code of BAYC: + ![BAYC Verified](/images/learn/etherscan/bayc-verified.png) + For a developer, verifying contracts is important since it gives transparency to your users. However, there are some risks because this means that bad actors can see the full source code and can try to exploit it. In order to read the state of the BAYC, you can go to the main menu and select the option **Read Contract**: + ![BAYC Read](/images/learn/etherscan/bayc-read.png) + After you select that option, you are able to see all of the read functions of the contract. You can also query who is the owner of the BAYC with id 150: + ![BAYC Query](/images/learn/etherscan/bayc-query.png) + ## Writing data to smart contracts using Etherscan @@ -88,13 +100,17 @@ In a similar fashion, you can read data from smart contracts using Etherscan. It To write data, go to the **Write Contract** tab: + ![Write Contract](/images/learn/etherscan/bayc-write.png) + From there, connect your wallet by clicking the **Connect with web3** button. After you connect, the following UI appears: + ![Write BAYC Connected](/images/learn/etherscan/bayc-write-connected.png) + You can then call the functions you wish to write to. diff --git a/docs/learn/hardhat/hardhat-deploy/hardhat-deploy-sbs.mdx b/docs/learn/hardhat/hardhat-deploy/hardhat-deploy-sbs.mdx index 880b53292..63a82808a 100644 --- a/docs/learn/hardhat/hardhat-deploy/hardhat-deploy-sbs.mdx +++ b/docs/learn/hardhat/hardhat-deploy/hardhat-deploy-sbs.mdx @@ -235,7 +235,9 @@ npx hardhat deploy --network base_sepolia After you run the command, a deployments folder appears with a newly-created deployment for `base_sepolia`: + ![New deployment](/images/learn/hardhat-deploying/new-deploy.png) + If you want to deploy to another network, change the network name as follows: diff --git a/docs/learn/hardhat/hardhat-forking/hardhat-forking.mdx b/docs/learn/hardhat/hardhat-forking/hardhat-forking.mdx index 120159e05..ba9941cda 100644 --- a/docs/learn/hardhat/hardhat-forking/hardhat-forking.mdx +++ b/docs/learn/hardhat/hardhat-forking/hardhat-forking.mdx @@ -145,7 +145,9 @@ describe('BalanceReader tests', () => { In this example, the [USDC address](https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48) is used and since USDC is an ERC-20 token, you can explore the token holders of that particular token directly in Etherscan: + ![Hardhat forking](/images/learn/hardhat-forking/hardhat-forking.png) + Or, visit https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48#balances, where you can see, at the time or writing, Arbitrum ONE Gateway is the top token holder. diff --git a/docs/learn/hardhat/hardhat-testing/hardhat-testing-sbs.mdx b/docs/learn/hardhat/hardhat-testing/hardhat-testing-sbs.mdx index 7fe366d15..804a4578a 100644 --- a/docs/learn/hardhat/hardhat-testing/hardhat-testing-sbs.mdx +++ b/docs/learn/hardhat/hardhat-testing/hardhat-testing-sbs.mdx @@ -165,7 +165,9 @@ it('should get the unlockTime value', async () => { Notice how autocomplete appears after entering `lockInstance`: + ![Auto complete](/images/learn/hardhat-testing/autocomplete-unlockTime.png) + You can simply run `npx hardhat test` and then get: diff --git a/docs/learn/hardhat/hardhat-tools-and-testing/analyzing-test-coverage.mdx b/docs/learn/hardhat/hardhat-tools-and-testing/analyzing-test-coverage.mdx index d04ade8c7..28ce7bd88 100644 --- a/docs/learn/hardhat/hardhat-tools-and-testing/analyzing-test-coverage.mdx +++ b/docs/learn/hardhat/hardhat-tools-and-testing/analyzing-test-coverage.mdx @@ -176,7 +176,9 @@ All files | 100 | 83.33 | 100 | 100 | | Which then gives you a report of the test coverage of your test suite. Notice there is a new folder called `coverage`, which was generated by the `solidity-coverage` plugin. Inside the `coverage` folder there is a `index.html` file. Open it in a browser, you'll see a report similar to the following: + ![Coverage report](/images/hardhat-test-coverage/coverage-report.png) + ## Increasing test coverage diff --git a/docs/learn/hardhat/hardhat-verify/hardhat-verify-sbs.mdx b/docs/learn/hardhat/hardhat-verify/hardhat-verify-sbs.mdx index ba738cd8c..bc9f5b70e 100644 --- a/docs/learn/hardhat/hardhat-verify/hardhat-verify-sbs.mdx +++ b/docs/learn/hardhat/hardhat-verify/hardhat-verify-sbs.mdx @@ -28,7 +28,9 @@ The way smart contracts are verified is by simply uploading the source code and Once the contract is verified, the Etherscan explorer shows a status like the following image: + ![Verified contract](https://github.com/base/web/blob/master/apps/base-docs/docs/public/images/learn/hardhat-verify/hardhat-verify.png) + Luckily, Hardhat and Hardhat-deploy already contain a built-in capability to do this task easily on your behalf. @@ -44,7 +46,9 @@ In order to obtain an Etherscan API key, visit [Etherscan](https://etherscan.io/ Then, go to [https://etherscan.io/myapikey](https://etherscan.io/myapikey) and create an API key by clicking the **Add** button: + ![Add key](https://github.com/base/web/blob/master/apps/base-docs/docs/public/images/learn/hardhat-verify/harhat-verify-create-key.png) + Bear in mind that different networks have other Blockchain explorers. For example: @@ -96,7 +100,9 @@ waiting for result... You can now go to Basescan and search for your contract address, where you'll see the following: + ![Base scan success](/images/learn/hardhat-verify/hardhat-verify-success.png) + ## Conclusion diff --git a/docs/learn/onchain-app-development/account-abstraction/account-abstraction-on-base-using-privy-and-the-base-paymaster.mdx b/docs/learn/onchain-app-development/account-abstraction/account-abstraction-on-base-using-privy-and-the-base-paymaster.mdx index 544a36336..5f86a1aa9 100644 --- a/docs/learn/onchain-app-development/account-abstraction/account-abstraction-on-base-using-privy-and-the-base-paymaster.mdx +++ b/docs/learn/onchain-app-development/account-abstraction/account-abstraction-on-base-using-privy-and-the-base-paymaster.mdx @@ -229,21 +229,29 @@ Finally, run `yarn dev` and navigate to [http://localhost:3000] to see the start Before exploring the code, test the app. First, you should see this login page: + ![Privy Login Page](/images/account-abstraction/privy-login-page.png) + After clicking "Log in" you'll see the following modal: + ![Privy Login Modal](/images/account-abstraction/privy-login-modal.png) + By default, you can login with a wallet, or email. After logging in, you'll be redirected to the `/dashboard` page, where the demo app will allow you to connect a number of other accounts to your `user` object: + ![Privy Dashboard Page](/images/account-abstraction/privy-dashboard-page.png) + If you navigate to [console.privy.io](https://console.privy.io/), you'll see that Privy stores all your users and their data here. + ![Privy Console](/images/account-abstraction/privy-console.png) + ### PrivyProvider @@ -278,7 +286,9 @@ Add a `config` property to the `` in `_app.jsx` with `'github'` Refresh to see that authentication is only possible now through Github or SMS: + ![Privy Login Methods](/images/account-abstraction/privy-login-methods.png) + You can find a full list of `loginMethods` in the docs for [`PrivyClientConfig`]. diff --git a/docs/learn/onchain-app-development/account-abstraction/gasless-transactions-with-paymaster.mdx b/docs/learn/onchain-app-development/account-abstraction/gasless-transactions-with-paymaster.mdx index 1023bdc51..b3a34b273 100644 --- a/docs/learn/onchain-app-development/account-abstraction/gasless-transactions-with-paymaster.mdx +++ b/docs/learn/onchain-app-development/account-abstraction/gasless-transactions-with-paymaster.mdx @@ -44,14 +44,20 @@ In this section, you will configure a Paymaster to sponsor payments on behalf of ### Screenshots -- **Selecting your project** +- **Selecting your project** + ![cdp-home.png](/images/gasless-transaction-on-base/cdp-select-project.png) + -- **Navigating to the Paymaster tool** +- **Navigating to the Paymaster tool** + ![cdp-paymaster-tool.png](/images/gasless-transaction-on-base/cdp-paymaster.png) + -- **Configuration screen** +- **Configuration screen** + ![cdp-paymaster-tool.png](/images/gasless-transaction-on-base/cdp-config.png) + ### Allowlist a Sponsorable Contract @@ -60,7 +66,9 @@ In this section, you will configure a Paymaster to sponsor payments on behalf of 3. Click **Add** to add an allowlisted contract. 4. For this example, add [`0x83bd615eb93eE1336acA53e185b03B54fF4A17e8`][simple NFT contract], and add the function `mintTo(address)`. + ![cdp-allowlist-contracts.png](/images/gasless-transaction-on-base/cdp-allowlist-contract.png) + > **Use your own contract** > We use a [simple NFT contract][simple NFT contract] on Base mainnet as an example. Feel free to substitute your own. @@ -84,7 +92,9 @@ This means **each user** can only have \$0.05 in sponsored gas and **1** user op Next, **Set the Global Limit**. For example, set this to `$0.07` so that once the entire paymaster has sponsored \$0.07 worth of gas (across all users), no more sponsorship occurs unless you raise the limit. + ![cdp-global-user-limits.png](/images/gasless-transaction-on-base/cdp-global-user-limits.png) + ## Test Your Paymaster Policy diff --git a/docs/learn/onchain-app-development/finance/build-a-smart-wallet-funding-app.mdx b/docs/learn/onchain-app-development/finance/build-a-smart-wallet-funding-app.mdx index 72772d3d6..e5323108c 100644 --- a/docs/learn/onchain-app-development/finance/build-a-smart-wallet-funding-app.mdx +++ b/docs/learn/onchain-app-development/finance/build-a-smart-wallet-funding-app.mdx @@ -36,7 +36,9 @@ You'll need to set up an account on with [Coinbase Developer Platform (CDP) Acco If you see a "something went wrong" error message when navigating to pay.coinbase.com, make sure you have "enforce secure initialization" disabled on the [Onramp config page] in Coinbase Developer Platform Dashboard. + ![fund-onramp-config](/images/onchainkit-tutorials/fund-onramp-config.png) + ## Setting up the Project @@ -113,7 +115,9 @@ Now that we know the user's balance, we can then have them mint an NFT or prompt The end state is to show their balance along with the appropriate call to actions like so: + ![fund-wallet](/images/onchainkit-tutorials/fund-wallet-balance.png) + Update your component's return statement with the following code: @@ -146,7 +150,9 @@ return ( Sweet! Now our conditional rendering is in full force. If a user clicks on the `+ Add funds to transact` button they will be given three options for topping up their smart wallet: + ![fund-wallet](/images/onchainkit-tutorials/fund-funding-options.png) + ## Conclusion diff --git a/docs/learn/onchain-app-development/frontend-setup/introduction-to-providers.mdx b/docs/learn/onchain-app-development/frontend-setup/introduction-to-providers.mdx index 6460ce71a..80ef241c9 100644 --- a/docs/learn/onchain-app-development/frontend-setup/introduction-to-providers.mdx +++ b/docs/learn/onchain-app-development/frontend-setup/introduction-to-providers.mdx @@ -133,11 +133,15 @@ Open up the [WalletConnect] homepage, and create an account, and/or sign in usin Click the `Create` button in the upper right of the `Projects` tab. + ![Create Button](/images/connecting-to-the-blockchain/wallet-connect-create-button.png) + Enter a name for your project, select the `App` option, and click `Create`. + ![Project Information](/images/connecting-to-the-blockchain/add-project-information.png) + Copy the _Project ID_ from the project information page, and paste it in as the `projectId` in `getDefaultWallets`. @@ -176,11 +180,15 @@ yarn run dev Click the `Connect Wallet` button, select your wallet from the modal, approve the connection, and you should see your network, token balance, and address or ENS name at the top of the screen. Select your wallet from the modal. + ![RainbowKit modal](/images/connecting-to-the-blockchain/rainbowkit-modal.png) + You've connected with the Public Provider! + ![Connected](/images/connecting-to-the-blockchain/connected.png) + ### QuickNode @@ -198,7 +206,9 @@ You'll need an RPC URL, so open up [QuickNode]'s site and sign up for an account On the next screen, you'll be asked to select a chain. Each endpoint only works for one. Select `Base`, click `Continue`. + ![Select Chain](/images/connecting-to-the-blockchain/quicknode-select-chain.png) + For now, pick `Base Mainnet`, but you'll probably want to delete this endpoint and create a new one for Sepolia when you start building. The free tier only allows you to have one at a time. @@ -229,7 +239,9 @@ To test this out, switch networks a few times. You'll know it's working if you s [Alchemy] is [no longer baked into wagmi], but it still works the same as any other RPC provider. As with QuickNode, you'll need an account and a key. Create an account and/or sign in, navigate to the `Apps` section in the left sidebar, and click `Create new app`. + ![Alchemy new app](/images/connecting-to-the-blockchain/alchemy-new-app.png) + Select Base Mainnet, and give your app a name. diff --git a/docs/learn/onchain-app-development/reading-and-displaying-data/useReadContract.mdx b/docs/learn/onchain-app-development/reading-and-displaying-data/useReadContract.mdx index b0dbd3b39..52e36ed11 100644 --- a/docs/learn/onchain-app-development/reading-and-displaying-data/useReadContract.mdx +++ b/docs/learn/onchain-app-development/reading-and-displaying-data/useReadContract.mdx @@ -174,7 +174,9 @@ Add in instance of your new component to `index.tsx`: Run your app, and you should see your list of issues fetched from the blockchain and displayed in the console! + ![Issues Console Log](/images/learn/reading-and-displaying-data/issues-console-log.png) + Breaking down the hook, you've: @@ -261,7 +263,9 @@ useEffect(() => { Everything appears to be working just fine, but how is `issueOne.desc` undefined? You can see it right there in the log! + ![Missing Data](/images/learn/reading-and-displaying-data/missing-data.png) + If you look closely, you'll see that `voters` is missing from the data in the logs. What's happening is that because the nested `mapping` cannot be returned outside the blockchain, it simply isn't. TypeScript then gets the `data` and does the best it can to cast it `as` an `Issue`. Since `voters` is missing, this will fail and it instead does the JavaScript trick of simply tacking on the extra properties onto the object. diff --git a/docs/learn/token-development/nft-guides/complex-onchain-nfts.mdx b/docs/learn/token-development/nft-guides/complex-onchain-nfts.mdx index f8747cc74..6b3d9bbf9 100644 --- a/docs/learn/token-development/nft-guides/complex-onchain-nfts.mdx +++ b/docs/learn/token-development/nft-guides/complex-onchain-nfts.mdx @@ -50,7 +50,9 @@ You can also work from ours: [Sample Art] Either way, you should end up with something similar to this: + ![Mockup](/images/onchain-generative-nfts/mockup.png) + ## The Art of Making it Fit @@ -74,7 +76,9 @@ If you don't have the tools to do this, you can find these files here: [Sample A You'll need to build and deploy a number of contracts for this project. They'll be organized in this architecture: + ![Architecture](/images/onchain-generative-nfts/architecture.png) + Deploying this many contracts will have a cost associated with it, but once they're deployed, this contract will cost the same as any other NFT contract. Remember, `pure` and `view` functions called outside the blockchain don't cost any gas. This means that you can use multiple contracts to assemble a relatively large graphic without additional costs! @@ -406,7 +410,9 @@ Open the contract in [Basescan], connect with your wallet, and mint some NFTs. **Wait a few minutes**, then open the [testnet version of Opensea] and look up your contract. It may take several minutes to show up, but when it does, if everything is working you'll see NFTs with the ocean part of the art! Neat! + ![First pass NFT](/images/onchain-generative-nfts/first_pass.png) + ## Adding the Sky Renderer @@ -761,7 +767,9 @@ const SVGRenderer = await deploy('SVGRenderer', { Test as before. It's starting to look really nice! + ![Progress](/images/onchain-generative-nfts/progress.png) + ## Adding the Sun Renderer diff --git a/docs/learn/token-development/nft-guides/dynamic-nfts.mdx b/docs/learn/token-development/nft-guides/dynamic-nfts.mdx index ac68e287e..fc1212449 100644 --- a/docs/learn/token-development/nft-guides/dynamic-nfts.mdx +++ b/docs/learn/token-development/nft-guides/dynamic-nfts.mdx @@ -13,7 +13,9 @@ hide_table_of_contents: false In this tutorial, you will create a dynamic NFT using Irys's [mutability features]. + ![Overview](/images/dynamic-nfts/all-characters.png) + Dynamic NFTs are NFTs whose metadata evolves over time. They are commonly used in: @@ -54,7 +56,9 @@ Irys has a pay-once-store-forever model and accepts payment for storage using mu Data on Irys is permanent and immutable, but you use Irys's [mutability features] to simulate mutability and create dynamic NFTs that evolve based on onchain or offchain actions. + ![Overview](/images/dynamic-nfts/mutable-references.png) + Using Irys's mutability features, you create a single, static URL that is linked to a series of transactions. Then, you can add a new transaction to the series at any time, and the URL will always resolve to the most recent transaction. @@ -127,7 +131,9 @@ irys -w -t base-eth ## Uploading the images + ![All NFTs](/images/dynamic-nfts/all-characters.png) + [Download a zip containing PNGs] for each level, and save them on your local drive. @@ -241,7 +247,9 @@ To mint your NFT in Remix: 3. Under the `Mint` function, enter the wallet address you want to mint the NFT to and the metadata URL (e.g. `https://gateway.irys.xyz/mutable/94TNg3UUKyZ96Dj8eSo9DVkBiivAz9jT39jjMFeTFvm3`) from the previous step. 4. Click Transact. + ![Image Level 3](/images/dynamic-nfts/open-sea-mockup.jpg) + You can now view the NFT on the [Opensea Testnet]. @@ -260,7 +268,9 @@ irys upload metadata-level-2.json \ Return to Opensea and request that it refresh your metadata. + ![Image Level 3](/images/dynamic-nfts/refresh-metadata.png) + Give it a few minutes and your updated NFT should be visible. diff --git a/docs/learn/token-development/nft-guides/simple-onchain-nfts.mdx b/docs/learn/token-development/nft-guides/simple-onchain-nfts.mdx index 15ec666bb..0b91ad887 100644 --- a/docs/learn/token-development/nft-guides/simple-onchain-nfts.mdx +++ b/docs/learn/token-development/nft-guides/simple-onchain-nfts.mdx @@ -288,7 +288,9 @@ Write some local tests, then [deploy] and test your contract. It can be very tri Remember, it can take a few minutes for them to register and add the collection. If the metadata or image don't show up correctly, use [Sepolia Basescan] to pull the `tokenURI` and an online or console base64 decoder to decode and check the json metadata and SVG image. + ![Random Color NFT](/images/smart-wallet/random-color-nft.png) + ## Conclusion diff --git a/docs/learn/token-development/nft-guides/thirdweb-unreal-nft-items.mdx b/docs/learn/token-development/nft-guides/thirdweb-unreal-nft-items.mdx index b30283240..5a2841112 100644 --- a/docs/learn/token-development/nft-guides/thirdweb-unreal-nft-items.mdx +++ b/docs/learn/token-development/nft-guides/thirdweb-unreal-nft-items.mdx @@ -11,7 +11,9 @@ author: briandoyle81 In this tutorial, you'll learn how to add NFT item usage on top of the demo game you build in their [Unreal Engine Quickstart]. Specifically, you'll use an NFT collection of random colors to change the color of the player's race car. + ![Color changing car nft](/images/build-with-thirdweb/car-color-nft.gif) + ## Objectives @@ -97,7 +99,9 @@ We do not have an official browser recommendation, but during our testing, Chrom Navigate to the [thirdweb engine dashboard], and click the `Import` button. Enter a name and the local address for your engine instance: + ![Add engine instance](/images/build-with-thirdweb/import-image-instance.png) + Next, you must add your wallet to the engine instance. Open up the instance in the dashboard, then click the `Import` button next to `Backend Wallets`. Enter your secret key for the wallet. @@ -175,7 +179,9 @@ If later in the tutorial, you get an error when you attempt to claim a token, bu Copy the address from the dashboard: + ![Token Airdrop Dashboard](/images/build-with-thirdweb/token-airdrop-dashboard.png) + Return to the `.env` for your server, and add: @@ -190,21 +196,29 @@ Run the client and server with `yarn client` and `yarn server`. Navigate to `loc Clone the thirdweb [Unreal Demo], and open it with the Unreal Editor. Do so by clicking the `Recent Projects` tab in the upper left, then `Browse`, in the lower right. + ![Open Unreal Project](/images/build-with-thirdweb/open-unreal-project.png) + Open the folder cloned from the repo and select `unreal_demo.uproject`. You may need to convert the project to the current version of Unreal. Click the `Open a copy` button. When the scene loads, double-click `Scene_Game` in the upper-right corner. + ![Scene Game](/images/build-with-thirdweb/scene-game.png) + Before you can play, you need to do some config. Scroll down in the `Outliner` until you find `ThirdWebManager`. Click the `Open Thirdweb Manager` button to open the file in your editor. + ![Open Thirdweb Manager](/images/build-with-thirdweb/open-thirdweb-manager.png) + Then, click the green play button at the top of the viewport. + ![Play Button](/images/build-with-thirdweb/play-button.png) + Log in using the credentials you created on the website, and play the game for a minute or two. If you get a 404, check that your engine, client, and server are all still running. @@ -295,7 +309,9 @@ router.post('/claim-random-color-nft', claimRandomColorNFT); Return to the Unreal Editor and open `ThirdwebManager.cpp`: + ![Open ThirdwebManager.cpp](/images/build-with-thirdweb/open-thirdweb-manager.png). + Similarly to what you did in the server, use the existing `PerformClaim()` as a template to add a function for `PerformNFTClaim()`. The only thing different is the name of the function and the URL: @@ -325,7 +341,9 @@ Open the `Content Drawer` at the bottom, search for `CollectibleNFT`, and drag o Find the `Perform Claim` function call and replace it with `Perform NFT Claim`. **Note** that the `Target` is passed from `Get Actor of Class`. + ![Perform NFT Claim](/images/build-with-thirdweb/perform-nft-claim.png) + You'll want to be able to tell this collectible apart, so click on the mesh for `Collectible` on the left side in the `Component` tree, then on the `Details` panel on the right, find the `Materials` section and change it to `MI_Solid_Blue`. @@ -563,7 +581,9 @@ Finally, drag off `Bind Event to OnNFTColorsResponse` and add a `Set Timer by Fu You should end up with something like this: + ![Get NFT Colors](/images/build-with-thirdweb/get-nft-colors.png) + Compile the blueprint then run the game. You should see that last color in the array in the HUD, and you should see the full list printed in the console every two seconds. @@ -607,7 +627,9 @@ Return to `Canvas_HUD` and open the `Graph`. Drag out of the `SetText` node that Finally, add a `Set Vector Parameter Value`. Select `NFT_MPS` for the collection and `Vector` for the `Parameter Name`. Connect the `Liner Color` output of `Hex String to Color` to the `Parameter Value` input. + ![Hex to linear color](/images/build-with-thirdweb/hex-to-linear-color.png) + Compile, save, and close `Canvas_HUD`. Run the game. Your car will start red, but after the response from the server, it will turn the color of your last NFT! Drive and collect the NFT collectible, and it will change colors! diff --git a/docs/mini-apps/technical-guides/neynar-notifications.mdx b/docs/mini-apps/technical-guides/neynar-notifications.mdx index c524e2310..09ca618d1 100644 --- a/docs/mini-apps/technical-guides/neynar-notifications.mdx +++ b/docs/mini-apps/technical-guides/neynar-notifications.mdx @@ -33,7 +33,9 @@ Navigate to [dev.neynar.com/app](https://dev.neynar.com/app) and then click on t Copy the url under **Mini app Notifications**. + ![neynar webhook url](/images/miniapps/neynar-notification-webhook.png) + @@ -230,15 +232,21 @@ https://dev.neynar.com/home + ![select app in neynar dev portal](/images/miniapps/neynar-select-app.png) + + ![select app in neynar dev portal](/images/miniapps/neynar-mini-app-tab.png) + + ![select app in neynar dev portal](/images/miniapps/neynar-send-notification.png) + The `target_fids` parameter is the starting point for all filtering. Pass an empty array for `target_fids` to start with the set of all FIDs with notifications enabled for your app, or manually define `target_fids` to list specific FIDs. diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000..18a441a27 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,48 @@ +# Scripts + +## MDX Linter + +Deterministic linter for MDX documentation files. + +### Usage + +```bash +# Check only files you've changed (default) +node scripts/lint-mdx.js + +# Check a specific file +node scripts/lint-mdx.js docs/cookbook/my-guide.mdx + +# Check a directory +node scripts/lint-mdx.js docs/onchainkit + +# Check all MDX files +node scripts/lint-mdx.js all +``` + +### Checks + +| Check | Severity | Description | +|-------|----------|-------------| +| Frontmatter | Error | `title` and `description` required | +| Headings | Error | Max one H1, no skipped levels | +| Headings | Warning | At least one heading per page (SEO) | +| Code blocks | Error | Language specifier required | +| Code blocks | Warning | Labels required in `` | +| Components | Warning | Required attributes on Mintlify components | +| Comments | Error | MDX `{/* */}` not HTML `` | +| Links | Warning | Internal links must point to existing files | + +### Exit codes + +| Code | Meaning | +|------|---------| +| `0` | No errors (warnings may exist) | +| `1` | Errors found | + +### CI Integration + +```bash +# Fail CI if linting errors exist +node scripts/lint-mdx.js all || exit 1 +``` diff --git a/scripts/lint-mdx.js b/scripts/lint-mdx.js new file mode 100755 index 000000000..07c42575b --- /dev/null +++ b/scripts/lint-mdx.js @@ -0,0 +1,480 @@ +#!/usr/bin/env node + +/** + * MDX Linter for Mintlify Documentation + * + * Deterministic checks for MDX files: + * - Frontmatter validation + * - Heading structure + * - Code block language specifiers + * - Mintlify component syntax + * - Internal link validation + * + * Usage: + * node scripts/lint-mdx.js # Check changed files only + * node scripts/lint-mdx.js all # Check all MDX files + * node scripts/lint-mdx.js docs/api # Check specific path + */ + +const fs = require("fs"); +const path = require("path"); +const { execSync } = require("child_process"); + +const DOCS_DIR = path.join(__dirname, "..", "docs"); + +// ----------------------------------------------------------------------------- +// File Discovery +// ----------------------------------------------------------------------------- + +function getChangedFiles() { + try { + const uncommitted = execSync("git diff --name-only HEAD", { + encoding: "utf-8", + cwd: path.join(__dirname, ".."), + }) + .trim() + .split("\n") + .filter(Boolean); + + const committed = execSync("git diff --name-only master...HEAD", { + encoding: "utf-8", + cwd: path.join(__dirname, ".."), + }) + .trim() + .split("\n") + .filter(Boolean); + + const allChanged = [...new Set([...uncommitted, ...committed])]; + return allChanged.filter( + (f) => f.startsWith("docs/") && f.endsWith(".mdx") + ); + } catch { + return []; + } +} + +function getAllMdxFiles(dir) { + const files = []; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...getAllMdxFiles(fullPath)); + } else if (entry.name.endsWith(".mdx")) { + files.push(path.relative(path.join(__dirname, ".."), fullPath)); + } + } + return files; +} + +function getFilesToCheck(arg) { + if (!arg) { + return { files: getChangedFiles(), mode: "changed" }; + } + if (arg === "all") { + return { files: getAllMdxFiles(DOCS_DIR), mode: "all" }; + } + // Specific path + const targetPath = path.join(__dirname, "..", arg); + if (fs.existsSync(targetPath)) { + if (fs.statSync(targetPath).isDirectory()) { + return { files: getAllMdxFiles(targetPath), mode: `path: ${arg}` }; + } + if (arg.endsWith(".mdx")) { + return { files: [arg], mode: `file: ${arg}` }; + } + } + return { files: [], mode: "invalid path" }; +} + +// ----------------------------------------------------------------------------- +// Linting Rules +// ----------------------------------------------------------------------------- + +function checkFrontmatter(content, filePath) { + const issues = []; + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + + if (!frontmatterMatch) { + issues.push({ line: 1, severity: "error", message: "Missing frontmatter" }); + return issues; + } + + const frontmatter = frontmatterMatch[1]; + + if (!/^title:\s*.+/m.test(frontmatter)) { + issues.push({ + line: 1, + severity: "error", + message: "Frontmatter missing required `title` field", + }); + } + + if (!/^description:\s*.+/m.test(frontmatter)) { + issues.push({ + line: 1, + severity: "error", + message: "Frontmatter missing required `description` field", + }); + } + + return issues; +} + +function checkHeadingStructure(content, filePath) { + const issues = []; + const lines = content.split("\n"); + let inCodeBlock = false; + let lastHeadingLevel = 0; + let h1Count = 0; + let totalHeadingCount = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line.startsWith("```")) { + inCodeBlock = !inCodeBlock; + continue; + } + if (inCodeBlock) continue; + + const headingMatch = line.match(/^(#{1,6})\s+/); + if (headingMatch) { + const level = headingMatch[1].length; + totalHeadingCount++; + + if (level === 1) { + h1Count++; + if (h1Count > 1) { + issues.push({ + line: i + 1, + severity: "error", + message: "Multiple H1 headings found (should have at most one)", + }); + } + } + + if (lastHeadingLevel > 0 && level > lastHeadingLevel + 1) { + issues.push({ + line: i + 1, + severity: "warning", + message: `Skipped heading level: H${lastHeadingLevel} → H${level}`, + }); + } + + lastHeadingLevel = level; + } + } + + // Check for pages with no headings (bad for SEO) + if (totalHeadingCount === 0) { + issues.push({ + line: 1, + severity: "warning", + message: "No headings found (at least one heading improves SEO)", + }); + } + + return issues; +} + +function checkCodeBlocks(content, filePath) { + const issues = []; + const lines = content.split("\n"); + let inCodeGroup = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line.includes("")) inCodeGroup = true; + if (line.includes("")) inCodeGroup = false; + + // Check for code block opening + const codeBlockMatch = line.match(/^```(\S*)/); + if (codeBlockMatch) { + const lang = codeBlockMatch[1]; + + // Check for empty language + if (!lang) { + issues.push({ + line: i + 1, + severity: "error", + message: "Code block missing language specifier", + }); + } + + // In CodeGroup, should have language AND label + if (inCodeGroup && lang && !lang.includes(" ") && !/\s+\S+/.test(line.slice(3 + lang.length))) { + // Check if there's a label after the language + const afterLang = line.slice(3 + lang.length).trim(); + if (!afterLang) { + issues.push({ + line: i + 1, + severity: "warning", + message: "Code block in should have a label (e.g., ```javascript Node.js)", + }); + } + } + } + } + + return issues; +} + +function checkMintlifyComponents(content, filePath) { + const issues = []; + const lines = content.split("\n"); + + // Track component nesting + const componentStack = []; + + // Components that need specific children + const parentChildRules = { + Steps: "Step", + Tabs: "Tab", + AccordionGroup: "Accordion", + }; + + // Required attributes + const requiredAttrs = { + Step: ["title"], + Tab: ["title"], + Accordion: ["title"], + Card: ["title"], + ParamField: ["type"], + ResponseField: ["name", "type"], + }; + + // Valid callout components + const validCallouts = ["Note", "Tip", "Warning", "Info", "Check"]; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Check for HTML comments + if (line.includes("", + }); + } + + // Check for typos in callouts + const calloutTypos = ["", "", "", "", ""]; + for (const typo of calloutTypos) { + if (line.includes(typo)) { + issues.push({ + line: i + 1, + severity: "error", + message: `Typo: ${typo} should be <${typo.slice(1, -2)}>`, + }); + } + } + + // Check opening tags + const openTagMatch = line.match(/<(Step|Tab|Accordion|Card|CardGroup|ParamField|ResponseField|Frame|Steps|Tabs|AccordionGroup)(\s[^>]*)?\/?>/); + if (openTagMatch) { + const tag = openTagMatch[1]; + const attrs = openTagMatch[2] || ""; + const isSelfClosing = line.includes("/>"); + + // Check required attributes + if (requiredAttrs[tag]) { + for (const attr of requiredAttrs[tag]) { + if (!new RegExp(`${attr}=`).test(attrs)) { + issues.push({ + line: i + 1, + severity: "warning", + message: `<${tag}> should have \`${attr}\` attribute`, + }); + } + } + } + + // CardGroup should have cols + if (tag === "CardGroup" && !attrs.includes("cols")) { + issues.push({ + line: i + 1, + severity: "warning", + message: " should have `cols` attribute", + }); + } + + // Track parent components + if (parentChildRules[tag] && !isSelfClosing) { + componentStack.push({ tag, line: i + 1 }); + } + } + + // Check for img without Frame + if (line.includes("= Math.max(0, i - 5); j--) { + if (lines[j].includes("", + }); + } + } + + // Check for img without alt + if (line.includes(" should have `alt` attribute", + }); + } + } + + return issues; +} + +function checkInternalLinks(content, filePath) { + const issues = []; + const lines = content.split("\n"); + + // Match markdown links and href attributes pointing to internal paths + const linkPatterns = [ + /\[([^\]]*)\]\(\/([^)#]+)/g, // [text](/path) + /href="\/([^"#]+)/g, // href="/path" + ]; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + for (const pattern of linkPatterns) { + let match; + pattern.lastIndex = 0; + + while ((match = pattern.exec(line)) !== null) { + const linkPath = match[pattern === linkPatterns[0] ? 2 : 1]; + + // Skip external-looking paths and anchors + if (linkPath.startsWith("http") || linkPath.startsWith("#")) continue; + + // Skip image paths + if (linkPath.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i)) continue; + + // Check if file exists + const possiblePaths = [ + path.join(DOCS_DIR, linkPath + ".mdx"), + path.join(DOCS_DIR, linkPath, "index.mdx"), + path.join(DOCS_DIR, linkPath), + ]; + + const exists = possiblePaths.some((p) => fs.existsSync(p)); + + if (!exists) { + issues.push({ + line: i + 1, + severity: "warning", + message: `Possibly broken internal link: /${linkPath}`, + }); + } + } + } + } + + return issues; +} + +// ----------------------------------------------------------------------------- +// Main +// ----------------------------------------------------------------------------- + +function lintFile(filePath) { + const fullPath = path.join(__dirname, "..", filePath); + if (!fs.existsSync(fullPath)) { + return [{ line: 0, severity: "error", message: "File not found" }]; + } + + const content = fs.readFileSync(fullPath, "utf-8"); + + const issues = [ + ...checkFrontmatter(content, filePath), + ...checkHeadingStructure(content, filePath), + ...checkCodeBlocks(content, filePath), + ...checkMintlifyComponents(content, filePath), + ...checkInternalLinks(content, filePath), + ]; + + return issues.sort((a, b) => a.line - b.line); +} + +function main() { + const arg = process.argv[2]; + const { files, mode } = getFilesToCheck(arg); + + console.log("## Lint Results\n"); + console.log(`### Files checked`); + console.log(`- ${files.length} files (${mode})`); + + if (files.length === 0) { + if (mode === "changed") { + console.log("- No changed MDX files found\n"); + } else { + console.log("- No files to check\n"); + } + console.log("### ✅ Summary"); + console.log("- 0 files checked, 0 errors, 0 warnings"); + process.exit(0); + } + + console.log(""); + + const allErrors = []; + const allWarnings = []; + + for (const file of files) { + const issues = lintFile(file); + for (const issue of issues) { + const entry = `\`${file}:${issue.line}\` — ${issue.message}`; + if (issue.severity === "error") { + allErrors.push(entry); + } else { + allWarnings.push(entry); + } + } + } + + if (allErrors.length > 0) { + console.log("### ❌ Errors (must fix)"); + for (const e of allErrors) { + console.log(`- ${e}`); + } + console.log(""); + } + + if (allWarnings.length > 0) { + console.log("### ⚠️ Warnings (should fix)"); + for (const w of allWarnings) { + console.log(`- ${w}`); + } + console.log(""); + } + + if (allErrors.length === 0 && allWarnings.length === 0) { + console.log("### ✅ All checks passed\n"); + } + + console.log("### Summary"); + console.log( + `- ${files.length} files checked, ${allErrors.length} errors, ${allWarnings.length} warnings` + ); + + // Exit with error code if there are errors + process.exit(allErrors.length > 0 ? 1 : 0); +} + +main();