Building OnChainNFT: A Simple Guide to On-Chain SVG NFTs

Creating NFTs with on-chain artwork is a powerful way to ensure the permanence and integrity of digital collectibles. This article walks through a simple implementation of an on-chain SVG NFT contract called "OnChainNFT" and explains the key concepts in easy-to-understand terms.
What Makes On-Chain NFTs Special?
Traditional NFTs typically store their artwork on IPFS or centralized servers, with only a reference to that location stored on the blockchain. This creates potential problems:
If the server goes down, the artwork could be lost
The referenced content could be changed
The link between the token and artwork might break
On-chain NFTs solve these problems by storing the artwork directly on the blockchain, creating a truly permanent and immutable digital asset.
Understanding the OnChainNFT Contract
Our OnChainNFT contract is a minimal implementation that demonstrates the core concepts of on-chain SVG storage. Let's break it down:
Contract Setup
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
contract OnChainNFT is ERC721, Ownable {
using Strings for uint256;
uint256 public price = 0.01 ether;
uint256 public totalSupply = 0;
uint256 public maxSupply = 100;
// Color choices for the NFTs
string[] public colors = ["red", "blue", "green", "purple", "orange"];
// Mapping from token ID to color index
mapping(uint256 => uint256) public tokenColors;
constructor() ERC721("OnChainNFT", "OCNFT") Ownable(msg.sender) {}
The contract:
Inherits from ERC721 (the NFT standard) and Ownable
Sets a mint price of 0.01 ETH
Caps the collection at 100 NFTs
Defines an array of color options
Creates a mapping to track which color is assigned to each token
Minting Function
function mint(uint256 colorIndex) external payable {
require(totalSupply < maxSupply, "Max supply reached");
require(msg.value >= price, "Insufficient ETH sent");
require(colorIndex < colors.length, "Invalid color index");
totalSupply++;
uint256 tokenId = totalSupply;
tokenColors[tokenId] = colorIndex;
_mint(msg.sender, tokenId);
}
The mint function:
Verifies we haven't hit the maximum supply
Checks that enough ETH was sent
Validates the color index is within range
Increments the supply counter
Stores the chosen color index
Mints the token to the sender
SVG Generation
function generateSVG(uint256 tokenId) internal view returns (string memory) {
string memory color = colors[tokenColors[tokenId]];
return string(abi.encodePacked(
'<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300" viewBox="0 0 300 300">',
'<rect width="100%" height="100%" fill="white" />',
'<circle cx="150" cy="150" r="120" fill="', color, '" />',
'<text x="150" y="150" font-family="Arial" font-size="20" text-anchor="middle" fill="white">',
'OnChainNFT #', tokenId.toString(),
'</text>',
'</svg>'
));
}
This function:
Gets the color for the specific token
Creates an SVG image with:
A white background
A colored circle in the middle
Text showing the NFT number
Uses string concatenation via
abi.encodePacked
The SVG is simple but effective - a colored circle with the token number displayed in the center.
The On-Chain Magic: tokenURI
function tokenURI(uint256 tokenId) public view override returns (string memory) {
require(_exists(tokenId), "Token does not exist");
string memory svg = generateSVG(tokenId);
string memory svgBase64 = Base64.encode(bytes(svg));
string memory json = string(abi.encodePacked(
'{',
'"name": "OnChainNFT #', tokenId.toString(), '",',
'"description": "A simple SVG NFT stored 100% on-chain",',
'"attributes": [{"trait_type": "Color", "value": "', colors[tokenColors[tokenId]], '"}],',
'"image": "data:image/svg+xml;base64,', svgBase64, '"',
'}'
));
string memory jsonBase64 = Base64.encode(bytes(json));
return string(abi.encodePacked('data:application/json;base64,', jsonBase64));
}
The tokenURI
function is where the on-chain storage happens:
It generates the SVG image
Encodes the SVG as Base64
Creates a JSON metadata object with:
Name: "OnChainNFT #[tokenId]"
Description
Attributes (color)
Image data URI with the Base64-encoded SVG
Encodes the entire JSON as Base64
Returns everything as a data URI
This approach stores both the image and metadata directly on the blockchain, making the NFT completely self-contained and immune to link rot.
How Data URIs Work
The key to on-chain NFTs is using data URIs, which embed the content directly in the URI itself. The format is:
data:[<mediatype>][;base64],<data>
For example, the SVG is embedded as:
data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDov...
And the entire metadata as:
data:application/json;base64,eyJuYW1lIjoiT25DaGFpbk...
NFT marketplaces and wallets can decode these URIs to display the image and metadata without needing to access any external servers.
Deployment Steps
To deploy your own OnChainNFT contract:
Set Up Your Environment
npm init -y npm install @openzeppelin/contracts hardhat npx hardhat init
Place Contract in Contracts Folder Save the contract code in a file named
OnChainNFT.sol
in your project's contracts directory.Deploy to a Test Network
npx hardhat run scripts/deploy.js --network rinkeby
Verify Your Contract Once deployed, verify your contract on Etherscan for transparency and easy interaction.
Cost Considerations
Storing data on-chain is gas-intensive. This simple implementation works well for basic SVGs, but for more complex artwork, consider:
Optimizing SVG code to be as compact as possible
Using layer 2 solutions like Polygon for lower gas fees
Creating algorithmic generation patterns that require minimal storage
Extending the Contract
This basic implementation can be extended in many ways:
Random Generation: Add randomness to create unique variations
Multiple Elements: Build SVGs with multiple components or layers
Animation: Include SVG animations for dynamic NFTs
Interactive Elements: Create SVGs that change based on blockchain data
Upgradable Art: Allow NFTs to evolve over time
Conclusion
On-chain SVG NFTs represent the gold standard of digital ownership. By storing artwork directly on the blockchain, they solve the permanence problems of traditional NFTs and create truly self-contained digital assets.
The simple OnChainNFT contract we've explored demonstrates the core principles of on-chain storage without unnecessary complexity. From this foundation, you can build more advanced implementations while maintaining the key benefit: complete on-chain storage.
Whether you're a creator looking to ensure the permanence of your digital art or a collector seeking truly immutable digital assets, on-chain SVG NFTs offer a compelling solution that aligns with the core promise of blockchain technology.
Subscribe to my newsletter
Read articles from Muhammed Musa directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Muhammed Musa
Muhammed Musa
From optimizing search results to building the future of the web - that's my journey. I'm Muhammed Musa, an SEO specialist with 5 years of experience, now venturing into the exciting realms of full-stack development and blockchain technology. I aim to blend my SEO expertise with cutting-edge web development and blockchain skills to create innovative, discoverable, decentralized solutions. I'm passionate about staying at the forefront of digital technology and eager to contribute to projects that push the boundaries of what's possible on the web.