diff --git a/.gitignore b/.gitignore index 7997bc8fa..8ab1d9c5a 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ yarn-error.log* .vercel next-env.d.ts .tmp* -._* \ No newline at end of file +._* +.serena \ No newline at end of file diff --git a/content/build/access/index.mdx b/content/build/access/index.mdx index 617e059ab..e4e5af78c 100644 --- a/content/build/access/index.mdx +++ b/content/build/access/index.mdx @@ -12,48 +12,21 @@ Once data is stored on Arweave, it's permanently available. Here's how to access Different methods serve different needs. Each provides unique capabilities for retrieving data from Arweave. - -

Search and discover data on Arweave

-
-
Query by tags and metadata
-
Filter by app, owner, timestamp
-
Get transaction IDs for fetching
-
- - } + description="Search and discover data on Arweave. Query by tags and metadata, filter by app/owner/timestamp, and get transaction IDs for fetching." href="/build/access/find-data" icon={} /> - -

Retrieve data bytes from Arweave

-
-
REST API endpoints
-
GET arweave.net/[txId]
-
Returns raw data/files
-
- - } + description="Retrieve data bytes from Arweave using REST API endpoints. Access via GET arweave.net/[txId] to get raw data and files." href="/build/access/fetch-data" icon={} /> - -

Assign names to data and apps

-
-
Create names like ardrive.ar.io
-
Point to any Arweave data
-
Update targets as needed
-
- - } + description="Assign names to data and apps. Create names like ardrive.ar.io, point to any Arweave data, and update targets as needed." href="/build/access/arns" icon={} /> diff --git a/content/build/guides/arns-marketplace.mdx b/content/build/guides/arns-marketplace.mdx index f2749ca34..49860fc2d 100644 --- a/content/build/guides/arns-marketplace.mdx +++ b/content/build/guides/arns-marketplace.mdx @@ -130,11 +130,11 @@ import { Globe, Code, Book } from "lucide-react";
} > - Check out hosting decentralized websites for content creation. + Check out hosting unstoppable apps for permanent website and dApp creation. - -``` - -Notes: -- ArNS undernames use underscores: `data_my-site`, not periods. -- Update the `data` record each time you publish. You can do this with your preferred tool (e.g., Permaweb Deploy, Turbo, etc). \ No newline at end of file diff --git a/content/build/guides/hosting-unstoppable-apps/deploying-with-arlink.mdx b/content/build/guides/hosting-unstoppable-apps/deploying-with-arlink.mdx new file mode 100644 index 000000000..af1a37271 --- /dev/null +++ b/content/build/guides/hosting-unstoppable-apps/deploying-with-arlink.mdx @@ -0,0 +1,310 @@ +--- +title: One Click Deployments with Arlink +description: Learn how to deploy unstoppable applications to Arweave using Arlink's visual web interface with automated builds and ArNS integration. +--- + +import { Callout } from 'fumadocs-ui/components/callout'; +import { Card, Cards } from 'fumadocs-ui/components/card'; +import { Steps, Step } from 'fumadocs-ui/components/steps'; +import { Zap, Github, Globe, Sparkles, FileCode, Rocket, GitBranch } from 'lucide-react'; + +![Arlink login page showing GitHub, Wander, and MetaMask authentication options](/arlink-homepage.png) +## Introduction + +[Arlink](https://arlink.ar.io) provides a visual, web-based platform for deploying applications to Arweave without using command-line tools. It offers automated builds, GitHub integration, and seamless ArNS management through an intuitive interface. + + +Check out the [series introduction](/build/guides/hosting-unstoppable-apps) to understand how AR.IO Network makes apps truly unstoppable with 100+ independent gateways. + + +## What You'll Learn + +- **Deploying with Arlink** - Using the visual web interface for deployments +- **GitHub integration** - Setting up automated deployments from your repository +- **ArNS setup** - Connecting your ArNS name or using free Arlink undernames +- **Build monitoring** - Tracking deployment progress with live logs +- **When to use Arlink** - Understanding limitations and choosing the right tool + +## What is Arlink? + +Arlink is a web-based deployment platform that simplifies hosting applications on Arweave. It provides a visual interface for developers who prefer graphical tools over command-line interfaces, while maintaining the same permanence and decentralization benefits. + +**Key Features:** + +- **Visual Interface** - No command-line knowledge required +- **Automated Builds** - Auto-detects build settings and handles the entire build process +- **GitHub Integration** - Deploy directly from your repositories with continuous deployment +- **Real-Time Monitoring** - Live build logs and progress tracking +- **ArNS Management** - Connect existing ArNS names or use free Arlink undernames + + + + +All applications that can be deployed with `permaweb-deploy` CLI can also be deployed with Arlink. Choose the tool that fits your workflow: +- **Arlink**: Visual workflows, quick deployments, smaller projects (under 10MB) +- **permaweb-deploy**: CI/CD pipelines, large applications, custom automation + +See [Limitations & Considerations](#limitations--considerations) below to help decide. + + +## Prerequisites + +Before deploying with Arlink, you'll need: + +- **Arweave Wallet** - Create one at [Wander](https://www.wander.app/) and add AR tokens or [Turbo credits](https://turbo.ar.io/topup) +- **GitHub Repository** - Your application code in a GitHub repository with build scripts +- **Static Build Output** - Application must build to static files (HTML, CSS, JS) +- **Optional: ArNS Name** - Purchase at [arns.app](https://arns.app) or use free Arlink undernames + + +Test your build locally (`npm run build`) before deploying to ensure it produces static output. + + +## Deployment Methods + +Arlink offers two main deployment approaches: + +- **GitHub Deploy** - Connect your repository for automated builds with continuous deployment +- **Template Hub** - Start with pre-built templates or add your own at [arlink.ar.io/templates](https://arlink.ar.io/templates) + +This guide focuses on GitHub deployment, which is the most common approach for custom applications. + +## Deploying from GitHub + +The deployment process consists of four main phases. For detailed step-by-step instructions, see the [Arlink Quickstart Guide](https://arlink.gitbook.io/arlink-docs/getting-started/quickstart). + +### 1. Connect & Authorize + +Navigate to the [Arlink Dashboard](https://arlink.arweave.net/) and connect your Arweave wallet (Wander). Then authorize GitHub access to enable repository connections. + +![Arlink login page showing GitHub, Wander, and MetaMask authentication options](/arlink-login.png) + + +Arlink only requests read access to your repositories and webhook permissions for continuous deployment. + + +### 2. Configure Deployment + +Select your GitHub repository and branch. Arlink will automatically detect: + +- **Package Manager** - npm, yarn, or pnpm +- **Framework** - React, Next.js, Vue, Astro, etc. +- **Build Command** - Usually `npm run build` +- **Output Directory** - `dist/`, `build/`, `out/`, etc. + +![Arlink repository selection interface showing GitHub repositories with import buttons](/arlink-repo-select.png) + +Review the auto-detected settings and adjust if needed. Ensure your output directory matches your framework: + +| Framework | Output Directory | Notes | +|-----------|-----------------|-------| +| Vite/React | `dist/` | Default configuration | +| Next.js | `out/` | Requires `output: 'export'` in config | +| Astro | `dist/` | Static by default | +| Create React App | `build/` | Default configuration | + +![Arlink deploy configuration options](/arlink-deploy-config.png) + +### 3. Choose Domain + +Select how your application will be accessible: + +**Free Arlink Undername:** +- Format: `yourname_arlink.arweave.net` +- No ArNS name purchase required +- Available immediately + +**Existing ArNS Name:** +- Use your purchased ArNS name (e.g., `myapp.arweave.net`) +- Optionally add undernames (e.g., `staging_myapp.arweave.net`) +- Arlink automatically updates ArNS records + +{/* Screenshot: Domain selection interface showing Arlink undername and existing ArNS options */} + +### 4. Deploy & Monitor + +Click **Deploy** to start the build process. Arlink will clone your repository, install dependencies, build your application, and upload to Arweave. + +![Arlink deployment build monitoring](/arlink-deployment-process.png) + +**Build Timeline:** +- Small apps (~1MB): 2-3 minutes +- Medium apps (1-5MB): 3-5 minutes +- Large apps (5-10MB): 5-10 minutes + + +Arlink enforces a **10MB max build output** and **10-minute build timeout**. For larger applications, use `permaweb-deploy` CLI instead. + + + +Once complete, your application is permanently deployed and accessible via: +- Your chosen domain (e.g., `myapp_arlink.arweave.net`) +- Any AR.IO gateway (e.g., `myapp_arlink.g8way.io`) +- Direct transaction ID + +{/* Screenshot: Successful deployment screen showing URL, transaction ID, and deployment stats */} + +## ArNS Integration + +Arlink offers two domain options for your deployments: + +### Free Arlink Undernames + +Arlink provides free subdomains under the `arlink` ArNS name: + +- **Format**: `yourname_arlink.arweave.net` +- **Cost**: Free (no ArNS purchase required) +- **Availability**: Instant, accessible via all AR.IO gateways +- **Limitation**: Must be unique across all Arlink deployments + +### Existing ArNS Names + +Connect your owned ArNS names for custom domains: + +- Select your ArNS name from the dashboard dropdown +- Optionally add undernames for versioning (e.g., `staging_myapp`, `v1_myapp`) +- Arlink automatically updates ArNS records on deployment + +{/* Screenshot: ArNS name selector showing owned names and undername input field */} + + +Undernames use underscore separators: `staging_myapp` not `staging.myapp`. See [Using Undernames for Versioning](/build/guides/hosting-unstoppable-apps/using-undernames-for-versioning) for versioning strategies. + + +**Deployment Management:** + +The Arlink dashboard lets you view deployment history, manage undernames, and rollback to previous deployments by updating which transaction ID your ArNS name points to. + +![Arlink deployment build monitoring](/arlink-history.png) + +## Limitations & Considerations + +Understanding Arlink's limitations helps you choose the right deployment tool for your project. + +### Size and Time Constraints + +| Constraint | Limit | Impact | +|------------|-------|--------| +| Max Build Output | 10 MB | Applications larger than 10MB cannot be deployed | +| Build Timeout | 10 minutes | Complex builds exceeding 10 minutes will fail | +| Deployment Cost | Subsidized (beta) | Pricing may change after beta period | + + +The 10MB limit applies to your **build output**, not your source code. Check your build size with: + +```bash +npm run build +du -sh dist/ # or build/, out/, etc. +``` + +If your build exceeds 10MB, use `permaweb-deploy` CLI instead. + + +### Comparison: Arlink vs permaweb-deploy CLI + +Choose the right tool for your use case: + + + } + title="Use Arlink When..." + description="You prefer visual interfaces over command-line tools, your build output is under 10MB, you want automated GitHub deployments, you need quick one-off deployments, you want to use free Arlink undernames, or your build completes in under 10 minutes." + /> + } + title="Use CLI When..." + description="Your build output exceeds 10MB, you need custom deployment scripts, you want CI/CD pipeline integration, you need Ethereum wallet deployment, you require Base-ETH payment options, or you want full control over deployment process." + /> + + +### Additional Limitations + +**Build Environment:** +- Standard Node.js environment only +- No custom build tools or dependencies +- Limited environment variable support +- No Docker or custom runtimes + +**Deployment Features:** +- No support for Ethereum wallet signatures +- No custom payment methods (Base-ETH, etc.) +- Limited automation beyond GitHub integration +- No programmatic API access + +**ArNS Management:** +- Cannot create new ArNS names through Arlink +- Must purchase ArNS names separately at [arns.app](https://arns.app) +- Limited undername configuration options + + +If you outgrow Arlink's capabilities, all your existing deployments can be managed with `permaweb-deploy` CLI. See the [other guides in this series](/build/guides/hosting-unstoppable-apps) for CLI deployment instructions. + + +## Continuous Deployment + +Arlink automatically sets up continuous deployment when you authorize GitHub access. + +### How It Works + +Arlink adds webhooks to your repository to detect push events. When you push to your configured branch, Arlink automatically triggers a new build and deployment. + +```bash +git add . +git commit -m "Update homepage content" +git push origin main # Triggers automatic deployment +``` + +{/* Screenshot: Webhook configuration settings in Arlink dashboard */} + +### Branch-Based Deployments + +Configure multiple branches to deploy to different undernames: + +| Branch | Undername | Purpose | +|--------|-----------|---------| +| `main` | `myapp` (root) | Production | +| `develop` | `staging_myapp` | Staging | +| `feature/*` | `dev_myapp` | Development | + +Monitor all deployments in the Arlink dashboard, which shows build status, commit hashes, build logs, and transaction IDs. + +For more details on continuous deployment setup, see the [Arlink Documentation](https://arlink.gitbook.io/arlink-docs). + +## Next Steps + +Now that you've learned how to deploy with Arlink, explore these related guides: + + + } + title="Using Undernames for Versioning" + description="Learn practical versioning strategies with ArNS undernames for staging environments, version history, and component architectures." + href="/build/guides/hosting-unstoppable-apps/using-undernames-for-versioning" + /> + } + title="Hosting a Blog" + description="Deploy static blogs with Next.js or Astro using permaweb-deploy CLI for more control and larger builds." + href="/build/guides/hosting-unstoppable-apps/hosting-a-blog" + /> + } + title="Hosting Arweave/AO dApp" + description="Build and deploy interactive dApps with AO integration, wallet connectivity, and GraphQL queries using CLI tools." + href="/build/guides/hosting-unstoppable-apps/hosting-arweave-ao-dapp" + /> + } + title="Working with ArNS" + description="Learn advanced ArNS management, custom domains, and programmatic record updates." + href="/build/guides/working-with-arns" + /> + + +## Resources + +- **Arlink Documentation**: [arlink.gitbook.io](https://arlink.gitbook.io/arlink-docs/getting-started/quickstart) +- **Arlink Dashboard**: [arlink.arweave.net](https://arlink.arweave.net/) +- **Template Hub**: [arlink.ar.io/templates](https://arlink.ar.io/templates) +- **ArNS App**: [arns.app](https://arns.app) +- **Turbo Credits**: [turbo.ar.io](https://turbo.ar.io/topup) diff --git a/content/build/guides/hosting-unstoppable-apps/hosting-a-blog.mdx b/content/build/guides/hosting-unstoppable-apps/hosting-a-blog.mdx new file mode 100644 index 000000000..fda11e96d --- /dev/null +++ b/content/build/guides/hosting-unstoppable-apps/hosting-a-blog.mdx @@ -0,0 +1,890 @@ +--- +title: "Hosting a Blog on ArNS" +description: "Learn how to deploy a permanent, censorship-resistant blog to Arweave with ArNS using permaweb-deploy" +--- + +import { Callout } from 'fumadocs-ui/components/callout'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Card, Cards } from 'fumadocs-ui/components/card'; +import { BookOpen, Code, GitBranch, Zap } from 'lucide-react'; + +## Introduction + +Deploying a blog to Arweave creates a **permanent, censorship-resistant website** that will be accessible forever through the AR.IO Network's 100+ independent gateways. Unlike traditional hosting where your content can disappear if you stop paying or if the host shuts down, content on Arweave is stored permanently for a one-time fee. + +This guide will teach you how to: +- Set up a static blog with Next.js or Astro +- Deploy it to Arweave using permaweb-deploy +- Connect it to your ArNS name for human-readable URLs +- Automate deployments with GitHub Actions + + +Check out the [series introduction](/build/guides/hosting-unstoppable-apps) to understand how AR.IO Network makes apps truly unstoppable with 100+ independent gateways. + + +## What You'll Build + +A permanent blog with: +- **Static site generator** (Next.js or Astro) optimized for content +- **Markdown-based posts** with frontmatter metadata +- **RSS feed** for content syndication +- **SEO optimization** with proper metadata +- **Permanent hosting** on Arweave accessible via your ArNS name +- **Automated deployments** with GitHub Actions + +By the end, you'll have a production-ready blog that will be accessible forever through the AR.IO Network's 100+ independent gateways. + +## Prerequisites + +Before starting, ensure you have: + +- **Node.js** (v18 or higher) and **npm** installed +- An **Arweave wallet** with storage credits + - [Create a wallet](https://www.wander.app/) with Wander if you don't have one + - [Topup storage credits](https://turbo.ar.io/topup) for uploading site to Arweave +- [Purchase an ArNS name](/build/guides/working-with-arns/purchase-arns-ui) for human-readable URLs +- Basic knowledge of **React/JavaScript** +- **Git** installed for version control + + +Never commit your wallet keyfile to version control. Always use environment variables or secure secret management for your private keys. + + +## Choose Your Framework + +This guide covers two popular static site generators optimized for blogs. Choose the one that fits your needs: + + + + **Best for:** React developers, dynamic features, large ecosystems + + Next.js is a powerful React framework with excellent blog support through static site generation (SSG). + + + **Best for:** Content-focused sites, minimal JavaScript, blazing-fast performance + + Astro is designed for content-heavy sites and ships zero JavaScript by default. + + + +## Project Setup + + + + ### Create a Next.js Blog + + + + ```bash + npx create-next-app@latest my-blog + ``` + + Choose these options: + - ✅ TypeScript + - ✅ ESLint + - ✅ Tailwind CSS + - ✅ `src/` directory + - ✅ App Router + - ❌ Customize default import alias + + + + ```bash + cd my-blog + ``` + + + + ```bash + npm install gray-matter remark remark-html + ``` + + These packages help parse Markdown blog posts. + + + + ```bash + mkdir -p src/app/blog src/posts + ``` + + + + Update `next.config.js`: + + ```javascript title="next.config.js" + /** @type {import('next').NextConfig} */ + const nextConfig = { + output: 'export', + images: { + unoptimized: true, + }, + trailingSlash: true, + } + + module.exports = nextConfig + ``` + + + The `output: 'export'` setting generates a static site that can be deployed to Arweave. The `images.unoptimized` setting is required since Next.js image optimization doesn't work with static exports. + + + + + Create `src/posts/first-post.md`: + + ```markdown title="src/posts/first-post.md" + --- + title: 'My First Permanent Blog Post' + date: '2024-01-15' + excerpt: 'This post will live forever on Arweave' + --- + + # Welcome to My Permanent Blog + + This blog post is stored permanently on Arweave and accessible through the AR.IO Network. + + ## Why This Matters + + Traditional blogs can disappear when: + - Hosting companies shut down + - You stop paying hosting fees + - Platforms change their terms + - Content gets censored + + **This post will be accessible forever.** + ``` + + + + Create `src/app/blog/page.tsx`: + + ```typescript title="src/app/blog/page.tsx" + import fs from 'fs'; + import path from 'path'; + import matter from 'gray-matter'; + import Link from 'next/link'; + + export default function BlogPage() { + const postsDirectory = path.join(process.cwd(), 'src/posts'); + const filenames = fs.readdirSync(postsDirectory); + + const posts = filenames.map((filename) => { + const filePath = path.join(postsDirectory, filename); + const fileContents = fs.readFileSync(filePath, 'utf8'); + const { data } = matter(fileContents); + + return { + slug: filename.replace('.md', ''), + title: data.title, + date: data.date, + excerpt: data.excerpt, + }; + }).sort((a, b) => (a.date < b.date ? 1 : -1)); + + return ( +
+

Blog Posts

+
+ {posts.map((post) => ( +
+ +

+ {post.title} +

+ +

{post.date}

+

{post.excerpt}

+
+ ))} +
+
+ ); + } + ``` +
+ + + Create `src/app/blog/[slug]/page.tsx`: + + ```typescript title="src/app/blog/[slug]/page.tsx" + import fs from 'fs'; + import path from 'path'; + import matter from 'gray-matter'; + import { remark } from 'remark'; + import html from 'remark-html'; + + export async function generateStaticParams() { + const postsDirectory = path.join(process.cwd(), 'src/posts'); + const filenames = fs.readdirSync(postsDirectory); + + return filenames.map((filename) => ({ + slug: filename.replace('.md', ''), + })); + } + + async function getPost(slug: string) { + const postsDirectory = path.join(process.cwd(), 'src/posts'); + const filePath = path.join(postsDirectory, `${slug}.md`); + const fileContents = fs.readFileSync(filePath, 'utf8'); + const { data, content } = matter(fileContents); + + const processedContent = await remark().use(html).process(content); + const contentHtml = processedContent.toString(); + + return { + slug, + contentHtml, + title: data.title, + date: data.date, + }; + } + + export default async function BlogPost({ + params, + }: { + params: { slug: string }; + }) { + const post = await getPost(params.slug); + + return ( +
+

{post.title}

+

{post.date}

+
+
+ ); + } + ``` +
+ + + ```bash + npm run dev + ``` + + Visit `http://localhost:3000/blog` to see your blog. + + + + ```bash + npm run build + ``` + + This creates an `out/` directory with your static site ready for deployment. + +
+
+ + + ### Create an Astro Blog + + + + ```bash + npm create astro@latest my-blog -- --template blog + ``` + + Choose these options: + - ✅ TypeScript (Strict) + - ✅ Install dependencies + - ✅ Initialize git repository + + + + ```bash + cd my-blog + ``` + + + + The blog template includes: + - `src/content/blog/` - Markdown blog posts + - `src/pages/blog/` - Blog listing and post pages + - `src/components/` - Reusable components + + + + Create `src/content/blog/first-post.md`: + + ```markdown title="src/content/blog/first-post.md" + --- + title: 'My First Permanent Blog Post' + description: 'This post will live forever on Arweave' + pubDate: 'Jan 15 2024' + heroImage: '/blog-placeholder.jpg' + --- + + # Welcome to My Permanent Blog + + This blog post is stored permanently on Arweave and accessible through the AR.IO Network. + + ## Why This Matters + + Traditional blogs can disappear when: + - Hosting companies shut down + - You stop paying hosting fees + - Platforms change their terms + - Content gets censored + + **This post will be accessible forever.** + ``` + + + + Update `astro.config.mjs`: + + ```javascript title="astro.config.mjs" + import { defineConfig } from 'astro/config'; + import mdx from '@astrojs/mdx'; + import sitemap from '@astrojs/sitemap'; + + export default defineConfig({ + site: 'https://myblog.arweave.net', + integrations: [mdx(), sitemap()], + build: { + format: 'directory', + }, + }); + ``` + + + Set the `site` field to your intended ArNS URL. This ensures proper URL generation for sitemaps and RSS feeds. + + + + + ```bash + npm run dev + ``` + + Visit `http://localhost:4321` to see your blog. + + + + ```bash + npm run build + ``` + + This creates a `dist/` directory with your static site ready for deployment. + + + +
+ +## Deploy to Arweave with permaweb-deploy + +Now that your blog is built, let's deploy it to Arweave using the `permaweb-deploy` CLI tool. + +### Install permaweb-deploy + + + + Install globally to use from any project: + + ```bash + npm install -g permaweb-deploy + ``` + + + Install in your project: + + ```bash + npm install --save-dev permaweb-deploy + ``` + + Then use via `npx permaweb-deploy` or add scripts to `package.json`. + + + +### Prepare Your Wallet + + + + From [Wander](https://www.wander.app/), export your wallet keyfile (JSON file). + + + + Save the keyfile **outside** your project directory: + + ```bash + # Example: Save to home directory + ~/wallets/arweave-wallet.json + ``` + + + Never commit your wallet keyfile to git! Always store wallets outside your project directory to prevent accidental commits. + + + + + As an extra safety measure, add wallet patterns to `.gitignore` to prevent accidentally committing wallet files if they're ever placed in your project: + + ```bash title=".gitignore" + # Arweave wallet files (safety precaution) + wallet*.json + arweave-keyfile*.json + *-wallet.json + ``` + + + Since your wallet is stored outside the project directory (`~/wallets/`), this is purely a safety precaution to protect you in case you accidentally copy or move a wallet file into your project. + + + + +### First Deployment + +You can deploy interactively (recommended for first time) or via CLI arguments. + + + + + + + + ```bash + permaweb-deploy + ``` + + + ```bash + permaweb-deploy + ``` + + + + + + The CLI will ask you: + + 1. **Build folder path?** + - Next.js: Enter `out` + - Astro: Enter `dist` + + 2. **Wallet path?** + - Enter path to your wallet keyfile: `~/wallets/arweave-wallet.json` + + 3. **Deploy to undername?** + - Enter `@` to deploy to your ArNS root domain + - Or enter undername like `v1` for versioning (more in [Guide 4](/build/guides/hosting-unstoppable-apps/using-undernames-for-versioning)) + + 4. **ArNS name?** + - Enter your ArNS name (e.g., `myblog`) + - This is the name you purchased from [ArNS Manager](https://arns.app) + + + + The tool will: + - ✅ Upload all files to Arweave + - ✅ Create a manifest + - ✅ Update your ArNS record + - ✅ Display the transaction IDs + + ```bash + Deployed TxId: abc123... + + Manifest TxId: xyz789... + Updated ANT record for @ to xyz789... + + View at: https://myblog.arweave.net + ``` + + + + Visit your ArNS URL (e.g., `https://myblog.arweave.net`) to see your live blog! + + + It may take 5-30 minutes for the content to be fully accessible as it propagates across the AR.IO Network's gateways. + + + + + + + + + + + ```bash + permaweb-deploy \ + --deploy-folder out \ + --arns-name my-app \ + --ttl 60 \ + --wallet ~/wallets/arweave-wallet.json + ``` + + + ```bash + permaweb-deploy \ + --deploy-folder dist \ + --arns-name my-app \ + --ttl 60 \ + --wallet ~/wallets/arweave-wallet.json + ``` + + + + + The `--ttl 60` flag sets the Time-To-Live to 60 seconds, controlling how quickly gateways update to your new deployment. This ensures your site updates within a minute of deployment. + + + + + The deployment will complete automatically and display the transaction IDs. + + + + Visit your ArNS URL to see your live blog! + + + + + +## Automating Deployments + +### Package Scripts + +Add deployment scripts to your `package.json` for quick redeployment: + + + + ```json title="package.json" + { + "scripts": { + "dev": "next dev", + "build": "next build", + "deploy": "npm run build && permaweb-deploy --deploy-folder out --arns-name my-app --ttl 60 --wallet $WALLET_PATH", + "deploy:staging": "npm run build && permaweb-deploy --deploy-folder out --arns-name my-app --undername staging --ttl 60 --wallet $WALLET_PATH" + } + } + ``` + + + ```json title="package.json" + { + "scripts": { + "dev": "astro dev", + "build": "astro build", + "deploy": "npm run build && permaweb-deploy --deploy-folder dist --arns-name $ARNS_NAME --ttl 60 --wallet $WALLET_PATH", + "deploy:staging": "npm run build && permaweb-deploy --deploy-folder dist --arns-name $ARNS_NAME --undername staging --ttl 60 --wallet $WALLET_PATH" + } + } + ``` + + + +Set environment variables: + +```bash +# Add to your shell profile (~/.bashrc, ~/.zshrc, etc.) +export ARNS_NAME="your-arns-name" +export WALLET_PATH="~/wallets/arweave-wallet.json" +``` + +Then deploy with: + +```bash +npm run deploy +``` + +### GitHub Actions + +For continuous deployment on every push to your repository: + + + + + 1. Go to your repository on GitHub + 2. Navigate to **Settings** > **Secrets and variables** > **Actions** + 3. Click **New repository secret** + 4. Name: `DEPLOY_KEY` + 5. Value: Paste your entire wallet keyfile JSON content + 6. Add another secret for `ARNS_NAME` with your ArNS name + + + + Create `.github/workflows/deploy.yml`: + + + + ```yaml title=".github/workflows/deploy.yml" + name: Deploy to Arweave + + on: + push: + branches: [main] + workflow_dispatch: + + jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build site + run: npm run build + + - name: Save wallet to file + run: echo '${{ secrets.DEPLOY_KEY }}' > wallet.json + + - name: Deploy to Arweave + run: | + npx permaweb-deploy \ + --deploy-folder out \ + --arns-name ${{ secrets.ARNS_NAME }} \ + --ttl 60 \ + --wallet wallet.json + + - name: Cleanup wallet + if: always() + run: rm -f wallet.json + ``` + + + ```yaml title=".github/workflows/deploy.yml" + name: Deploy to Arweave + + on: + push: + branches: [main] + workflow_dispatch: + + jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build site + run: npm run build + + - name: Save wallet to file + run: echo '${{ secrets.DEPLOY_KEY }}' > wallet.json + + - name: Deploy to Arweave + run: | + npx permaweb-deploy \ + --deploy-folder dist \ + --arns_name ${{ secrets.ARNS_NAME }} \ + --ttl 60 \ + --wallet wallet.json + + - name: Cleanup wallet + if: always() + run: rm -f wallet.json + ``` + + + + + + ```bash + git add .github/workflows/deploy.yml + git commit -m "Add automated deployment to Arweave" + git push origin main + ``` + + + + - Go to your repository's **Actions** tab + - Watch the deployment workflow run + - See the deployment logs in real-time + + + + +The workflow runs on every push to `main`. You can also trigger it manually from the Actions tab using `workflow_dispatch`. + + +## Blog-Specific Considerations + +### Images and Assets + +For permanent blogs, store images directly in your repository: + + + + ``` + public/ + ├── images/ + │ ├── blog/ + │ │ ├── post1-hero.jpg + │ │ └── post1-diagram.png + ``` + + Reference in Markdown: + + ```markdown + ![My Image](/images/blog/post1-hero.jpg) + ``` + + Or use Next.js Image component in MDX: + + ```jsx + import Image from 'next/image'; + + Hero image + ``` + + + ``` + public/ + ├── images/ + │ ├── blog/ + │ │ ├── post1-hero.jpg + │ │ └── post1-diagram.png + ``` + + Reference in Markdown: + + ```markdown + ![My Image](/images/blog/post1-hero.jpg) + ``` + + Or use Astro's Image component: + + ```astro + --- + import { Image } from 'astro:assets'; + import heroImage from '../assets/hero.jpg'; + --- + + Hero image + ``` + + + + +Avoid external image links (like from CDNs) as they may not be permanent. Store all assets in your repository for true permanence. + + +### SEO and Metadata + +Add proper metadata to each blog post: + + + + Update `src/app/blog/[slug]/page.tsx`: + + ```typescript + export async function generateMetadata({ + params, + }: { + params: { slug: string }; + }) { + const post = await getPost(params.slug); + + return { + title: post.title, + description: post.excerpt, + openGraph: { + title: post.title, + description: post.excerpt, + type: 'article', + publishedTime: post.date, + }, + }; + } + ``` + + + Add to frontmatter in your posts: + + ```markdown + --- + title: 'My Blog Post' + description: 'This is the meta description' + pubDate: 'Jan 15 2024' + author: 'Your Name' + image: '/images/blog/post1-hero.jpg' + tags: ['arweave', 'web3', 'blogging'] + --- + ``` + + + +### RSS Feed (Optional) + +For Astro, RSS is easy to add: + +```bash +npm install @astrojs/rss +``` + +Create `src/pages/rss.xml.js`: + +```javascript +import rss from '@astrojs/rss'; +import { getCollection } from 'astro:content'; + +export async function GET(context) { + const posts = await getCollection('blog'); + return rss({ + title: 'My Permanent Blog', + description: 'A blog hosted permanently on Arweave', + site: context.site, + items: posts.map((post) => ({ + title: post.data.title, + pubDate: post.data.pubDate, + description: post.data.description, + link: `/blog/${post.slug}/`, + })), + }); +} +``` + +## Next Steps + +Congratulations! You've deployed a permanent blog to Arweave. Here's what to explore next: + + + } + /> + } + /> + } + /> + diff --git a/content/build/guides/hosting-unstoppable-apps/hosting-arweave-ao-dapp.mdx b/content/build/guides/hosting-unstoppable-apps/hosting-arweave-ao-dapp.mdx new file mode 100644 index 000000000..e03c600d2 --- /dev/null +++ b/content/build/guides/hosting-unstoppable-apps/hosting-arweave-ao-dapp.mdx @@ -0,0 +1,1518 @@ +--- +title: "Hosting an Arweave/AO App on ArNS" +description: "Learn how to build and deploy decentralized applications that interact with Arweave and AO processes" +--- + +import { Callout } from 'fumadocs-ui/components/callout'; +import { Steps, Step } from 'fumadocs-ui/components/steps'; +import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; +import { Cards, Card } from 'fumadocs-ui/components/card'; +import { Code, Wallet, Database, GitBranch, Zap, Terminal, FileCode, Globe } from 'lucide-react'; + +## Introduction + +This guide walks you through building and deploying a decentralized application (dApp) that interacts with the Arweave and AO ecosystems. You'll create a functional token dashboard for the ARIO token that connects to user wallets, interacts with the ARIO AO process, and queries data from Arweave. + + +Check out the [series introduction](/build/guides/hosting-unstoppable-apps) to understand how AR.IO Network makes apps truly unstoppable with 100+ independent gateways. + + +## What You'll Build + +In this guide, you'll create an **AO Token Dashboard for the ARIO token** - a dApp that: +- Connects to Wander wallet +- Fetches ARIO token balances from the ARIO process +- Sends ARIO tokens to other addresses +- Queries transaction data from Arweave using GraphQL +- Displays token information in a responsive interface + + +This guide uses the ARIO token (`qNvAoz0TgcH7DMg8BCVn8jF32QH5L6T29VjHxhHqqGE`) as a practical example. The same patterns work for any AO token - simply replace the process ID with your target token's process ID. + + +## Prerequisites + +Before starting, ensure you have: + +- **Node.js 18+** and npm installed +- **Git** for version control +- **Basic knowledge** of React and TypeScript +- **Wander wallet** extension installed ([download here](https://wander.app/download)) +- **Arweave wallet** with storage credits for deployment ([create with Wander](https://www.wander.app/)) +- **Storage credits** from [turbo.ar.io](https://turbo.ar.io/topup) +- **ArNS name** registered (see [Purchase ArNS Name](/build/guides/working-with-arns/purchase-arns-ui)) + + +If you're new to AO, check out the [AO documentation](https://cookbook_ao.g8way.io/) to understand the architecture and concepts. + + +## Setting Up Your Development Environment + +Let's start by creating a new React project with Vite and installing the necessary dependencies. + + + +```bash +npm create vite@latest my-ao-dapp -- --template react-ts +cd my-ao-dapp +npm install +``` + + + +```bash +npm install @permaweb/aoconnect arconnect graphql-request +``` + +**Package explanations:** +- `@permaweb/aoconnect` - SDK for interacting with AO processes +- `arconnect` - Type definitions for Wander wallet (maintained for compatibility) +- `graphql-request` - Lightweight GraphQL client for Arweave queries + + + +```bash +npm install --save-dev permaweb-deploy +``` + + + +## Project Structure + +Organize your project with this recommended structure: + +``` +my-ao-dapp/ +├── src/ +│ ├── components/ +│ │ ├── WalletConnect.tsx # Wallet connection component +│ │ ├── TokenBalance.tsx # Token balance display +│ │ ├── SendTokens.tsx # Token sending form +│ │ └── TransactionList.tsx # Transaction history +│ ├── lib/ +│ │ ├── ao.ts # AO process interactions +│ │ ├── arweave.ts # Arweave GraphQL queries +│ │ └── wallet.ts # Wallet utilities +│ ├── types/ +│ │ └── index.ts # TypeScript type definitions +│ ├── App.tsx # Main application component +│ ├── App.css # Application styles +│ └── main.tsx # Application entry point +├── dist/ # Build output directory +├── package.json +├── tsconfig.json +├── vite.config.ts +└── index.html +``` + +## Wallet Integration + +First, let's create wallet connection functionality using Wander. + +### Create wallet utilities + +Create `src/lib/wallet.ts`: + +```typescript +// Check if Wander is available +export const isWanderInstalled = (): boolean => { + return typeof window !== 'undefined' && 'arweaveWallet' in window; +}; + +// Connect to Wander wallet +export const connectWallet = async (): Promise => { + if (!isWanderInstalled()) { + throw new Error('Wander is not installed. Please install the extension.'); + } + + try { + // Request permissions from user + await window.arweaveWallet.connect( + ['ACCESS_ADDRESS', 'SIGN_TRANSACTION', 'ACCESS_PUBLIC_KEY'], + { + name: 'ARIO Token Dashboard', + logo: 'https://arweave.net/your-logo-tx-id' + } + ); + + // Get the wallet address + const address = await window.arweaveWallet.getActiveAddress(); + return address; + } catch (error) { + console.error('Failed to connect wallet:', error); + throw error; + } +}; + +// Disconnect wallet +export const disconnectWallet = async (): Promise => { + if (isWanderInstalled()) { + await window.arweaveWallet.disconnect(); + } +}; + +// Get current wallet address +export const getWalletAddress = async (): Promise => { + if (!isWanderInstalled()) { + return null; + } + + try { + const address = await window.arweaveWallet.getActiveAddress(); + return address; + } catch { + return null; + } +}; +``` + +### Create wallet connection component + +Create `src/components/WalletConnect.tsx`: + +```typescript +import { useState, useEffect } from 'react'; +import { connectWallet, disconnectWallet, getWalletAddress, isWanderInstalled } from '../lib/wallet'; + +interface WalletConnectProps { + onAddressChange: (address: string | null) => void; +} + +export function WalletConnect({ onAddressChange }: WalletConnectProps) { + const [address, setAddress] = useState(null); + const [isConnecting, setIsConnecting] = useState(false); + const [error, setError] = useState(null); + + // Check for existing connection on mount + useEffect(() => { + checkConnection(); + }, []); + + const checkConnection = async () => { + const addr = await getWalletAddress(); + setAddress(addr); + onAddressChange(addr); + }; + + const handleConnect = async () => { + if (!isWanderInstalled()) { + setError('Wander extension is not installed'); + return; + } + + setIsConnecting(true); + setError(null); + + try { + const addr = await connectWallet(); + setAddress(addr); + onAddressChange(addr); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to connect wallet'); + setAddress(null); + onAddressChange(null); + } finally { + setIsConnecting(false); + } + }; + + const handleDisconnect = async () => { + await disconnectWallet(); + setAddress(null); + onAddressChange(null); + }; + + if (!isWanderInstalled()) { + return ( +
+

Wander extension not found

+ + Install Wander + +
+ ); + } + + return ( +
+ {error &&

{error}

} + + {address ? ( +
+

Connected: {address.slice(0, 8)}...{address.slice(-8)}

+ +
+ ) : ( + + )} +
+ ); +} +``` + +### Add TypeScript declarations + +Create `src/types/index.ts`: + +```typescript +// Wander wallet type declaration (uses window.arweaveWallet API) +// Note: Wander was formerly ArConnect - the API and TypeScript types remain compatible +declare global { + interface Window { + arweaveWallet: { + connect( + permissions: string[], + appInfo?: { name: string; logo: string } + ): Promise; + disconnect(): Promise; + getActiveAddress(): Promise; + getActivePublicKey(): Promise; + sign( + transaction: unknown, + options?: { tags?: { name: string; value: string }[] } + ): Promise; + }; + } +} + +// AO Process types +export interface AOMessage { + Target: string; + Action: string; + [key: string]: string | number; +} + +export interface AOResult { + Messages: Array<{ + Data: string; + Tags: Array<{ name: string; value: string }>; + }>; + Error?: string; +} + +// Token types +export interface TokenBalance { + process: string; + balance: string; + ticker: string; + name: string; + denomination: number; +} + +// Transaction types +export interface ArweaveTransaction { + id: string; + owner: { + address: string; + }; + tags: Array<{ + name: string; + value: string; + }>; + block?: { + timestamp: number; + height: number; + }; +} + +export {}; +``` + +## AO Process Interaction + +Now let's create functions to interact with AO processes. + +### Create AO utilities + +Create `src/lib/ao.ts`: + +```typescript +import { createDataItemSigner, message, result } from '@permaweb/aoconnect'; +import type { AOMessage, AOResult, TokenBalance } from '../types'; + +// Send a message to an AO process +export const sendAOMessage = async ( + processId: string, + messageData: AOMessage +): Promise => { + try { + // Create a signer using Wander + const signer = createDataItemSigner(window.arweaveWallet); + + // Send the message + const messageId = await message({ + process: processId, + tags: Object.entries(messageData).map(([name, value]) => ({ + name, + value: String(value) + })), + signer, + data: messageData.Data || '' + }); + + return messageId; + } catch (error) { + console.error('Failed to send AO message:', error); + throw error; + } +}; + +// Get result from an AO message +export const getAOResult = async ( + processId: string, + messageId: string +): Promise => { + try { + const res = await result({ + process: processId, + message: messageId + }); + + return res as AOResult; + } catch (error) { + console.error('Failed to get AO result:', error); + throw error; + } +}; + +// Get token balance from an AO token process +export const getTokenBalance = async ( + processId: string, + address: string +): Promise => { + try { + // Create a signer + const signer = createDataItemSigner(window.arweaveWallet); + + // Send balance query message + const messageId = await message({ + process: processId, + tags: [ + { name: 'Action', value: 'Balance' }, + { name: 'Target', value: address } + ], + signer + }); + + // Wait a moment for processing + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Get the result + const res = await result({ + process: processId, + message: messageId + }); + + // Parse the result + const data = JSON.parse(res.Messages[0].Data); + + return { + process: processId, + balance: data.balance || '0', + ticker: data.ticker || 'TOKEN', + name: data.name || 'Unknown Token', + denomination: data.denomination || 12 + }; + } catch (error) { + console.error('Failed to get token balance:', error); + throw error; + } +}; + +// Send tokens to another address +export const sendTokens = async ( + processId: string, + recipient: string, + amount: string +): Promise => { + try { + const signer = createDataItemSigner(window.arweaveWallet); + + const messageId = await message({ + process: processId, + tags: [ + { name: 'Action', value: 'Transfer' }, + { name: 'Recipient', value: recipient }, + { name: 'Quantity', value: amount } + ], + signer + }); + + return messageId; + } catch (error) { + console.error('Failed to send tokens:', error); + throw error; + } +}; + +// Get token info from process +export const getTokenInfo = async (processId: string): Promise<{ + name: string; + ticker: string; + denomination: number; + logo?: string; +}> => { + try { + const signer = createDataItemSigner(window.arweaveWallet); + + const messageId = await message({ + process: processId, + tags: [{ name: 'Action', value: 'Info' }], + signer + }); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const res = await result({ + process: processId, + message: messageId + }); + + const data = JSON.parse(res.Messages[0].Data); + + return { + name: data.Name || 'Unknown', + ticker: data.Ticker || 'TOKEN', + denomination: data.Denomination || 12, + logo: data.Logo + }; + } catch (error) { + console.error('Failed to get token info:', error); + throw error; + } +}; +``` + +## GraphQL Queries for Arweave Data + +Let's create utilities to query transaction data from Arweave using GraphQL. + +### Create Arweave query utilities + +Create `src/lib/arweave.ts`: + +```typescript +import { GraphQLClient, gql } from 'graphql-request'; +import type { ArweaveTransaction } from '../types'; + +// Initialize GraphQL client +const client = new GraphQLClient('https://arweave.net/graphql'); + +// Query transactions by owner +export const getTransactionsByOwner = async ( + owner: string, + limit: number = 10 +): Promise => { + const query = gql` + query GetTransactions($owner: String!, $first: Int!) { + transactions( + owners: [$owner] + first: $first + sort: HEIGHT_DESC + ) { + edges { + node { + id + owner { + address + } + tags { + name + value + } + block { + timestamp + height + } + } + } + } + } + `; + + try { + const data = await client.request<{ + transactions: { + edges: Array<{ node: ArweaveTransaction }>; + }; + }>(query, { owner, first: limit }); + + return data.transactions.edges.map(edge => edge.node); + } catch (error) { + console.error('Failed to query transactions:', error); + throw error; + } +}; + +// Query transactions by tags +export const getTransactionsByTags = async ( + tags: Array<{ name: string; values: string[] }>, + limit: number = 10 +): Promise => { + const query = gql` + query GetTransactionsByTags($tags: [TagFilter!]!, $first: Int!) { + transactions( + tags: $tags + first: $first + sort: HEIGHT_DESC + ) { + edges { + node { + id + owner { + address + } + tags { + name + value + } + block { + timestamp + height + } + } + } + } + } + `; + + try { + const data = await client.request<{ + transactions: { + edges: Array<{ node: ArweaveTransaction }>; + }; + }>(query, { tags, first: limit }); + + return data.transactions.edges.map(edge => edge.node); + } catch (error) { + console.error('Failed to query transactions by tags:', error); + throw error; + } +}; + +// Get transaction data +export const getTransactionData = async (txId: string): Promise => { + try { + const response = await fetch(`https://arweave.net/${txId}`); + if (!response.ok) { + throw new Error(`Failed to fetch transaction data: ${response.statusText}`); + } + return await response.text(); + } catch (error) { + console.error('Failed to get transaction data:', error); + throw error; + } +}; + +// Format timestamp to readable date +export const formatTimestamp = (timestamp: number): string => { + return new Date(timestamp * 1000).toLocaleString(); +}; +``` + +## Building the Dashboard Components + +Now let's create the components for our token dashboard. + +### Token Balance Component + +Create `src/components/TokenBalance.tsx`: + +```typescript +import { useState, useEffect } from 'react'; +import { getTokenBalance } from '../lib/ao'; +import type { TokenBalance } from '../types'; + +interface TokenBalanceProps { + processId: string; + address: string; +} + +export function TokenBalance({ processId, address }: TokenBalanceProps) { + const [balance, setBalance] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadBalance(); + }, [processId, address]); + + const loadBalance = async () => { + setLoading(true); + setError(null); + + try { + const bal = await getTokenBalance(processId, address); + setBalance(bal); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load balance'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return
Loading balance...
; + } + + if (error) { + return
Error: {error}
; + } + + if (!balance) { + return null; + } + + // Format balance with denomination + const formattedBalance = ( + Number(balance.balance) / Math.pow(10, balance.denomination) + ).toFixed(balance.denomination); + + return ( +
+

{balance.name} ({balance.ticker})

+

{formattedBalance} {balance.ticker}

+

Process: {processId.slice(0, 8)}...{processId.slice(-8)}

+ +
+ ); +} +``` + +### Send Tokens Component + +Create `src/components/SendTokens.tsx`: + +```typescript +import { useState } from 'react'; +import { sendTokens } from '../lib/ao'; + +interface SendTokensProps { + processId: string; + onSuccess: () => void; +} + +export function SendTokens({ processId, onSuccess }: SendTokensProps) { + const [recipient, setRecipient] = useState(''); + const [amount, setAmount] = useState(''); + const [sending, setSending] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!recipient || !amount) { + setError('Please fill in all fields'); + return; + } + + setSending(true); + setError(null); + setSuccess(null); + + try { + const messageId = await sendTokens(processId, recipient, amount); + setSuccess(`Tokens sent! Message ID: ${messageId}`); + setRecipient(''); + setAmount(''); + + // Wait a moment then refresh parent + setTimeout(() => { + onSuccess(); + }, 2000); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to send tokens'); + } finally { + setSending(false); + } + }; + + return ( +
+

Send Tokens

+ + {error &&

{error}

} + {success &&

{success}

} + +
+
+ + setRecipient(e.target.value)} + placeholder="Enter Arweave address" + disabled={sending} + /> +
+ +
+ + setAmount(e.target.value)} + placeholder="Enter amount" + disabled={sending} + /> +
+ + +
+
+ ); +} +``` + +### Transaction List Component + +Create `src/components/TransactionList.tsx`: + +```typescript +import { useState, useEffect } from 'react'; +import { getTransactionsByOwner, formatTimestamp } from '../lib/arweave'; +import type { ArweaveTransaction } from '../types'; + +interface TransactionListProps { + address: string; +} + +export function TransactionList({ address }: TransactionListProps) { + const [transactions, setTransactions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadTransactions(); + }, [address]); + + const loadTransactions = async () => { + setLoading(true); + setError(null); + + try { + const txs = await getTransactionsByOwner(address, 10); + setTransactions(txs); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load transactions'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return
Loading transactions...
; + } + + if (error) { + return
Error: {error}
; + } + + if (transactions.length === 0) { + return
No transactions found
; + } + + return ( +
+

Recent Transactions

+
+ {transactions.map((tx) => ( +
+ + {tx.block && ( +
+ {formatTimestamp(tx.block.timestamp)} +
+ )} +
+ {tx.tags.slice(0, 3).map((tag, i) => ( + + {tag.name}: {tag.value} + + ))} +
+
+ ))} +
+ +
+ ); +} +``` + +## Main Application Component + +Now let's put it all together in the main App component. + +Update `src/App.tsx`: + +```typescript +import { useState } from 'react'; +import { WalletConnect } from './components/WalletConnect'; +import { TokenBalance } from './components/TokenBalance'; +import { SendTokens } from './components/SendTokens'; +import { TransactionList } from './components/TransactionList'; +import './App.css'; + +function App() { + const [walletAddress, setWalletAddress] = useState(null); + const [refreshKey, setRefreshKey] = useState(0); + + // ARIO token process ID + const ARIO_PROCESS_ID = 'qNvAoz0TgcH7DMg8BCVn8jF32QH5L6T29VjHxhHqqGE'; + + const handleBalanceRefresh = () => { + setRefreshKey(prev => prev + 1); + }; + + return ( +
+
+

ARIO Token Dashboard

+ +
+ +
+ {walletAddress ? ( + <> +
+ +
+ +
+ +
+ +
+ +
+ + ) : ( +
+

Connect your wallet to view your ARIO token balance and send tokens

+
+ )} +
+ +
+

Powered by Arweave & AO

+
+
+ ); +} + +export default App; +``` + +### Add basic styling + +Update `src/App.css`: + +```css +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0a0a0a; + color: #ffffff; +} + +.app { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.app-header { + padding: 2rem; + background: #111; + border-bottom: 1px solid #333; + display: flex; + justify-content: space-between; + align-items: center; +} + +.app-header h1 { + font-size: 1.5rem; + font-weight: 600; +} + +.app-main { + flex: 1; + padding: 2rem; + max-width: 1200px; + margin: 0 auto; + width: 100%; +} + +.section { + background: #111; + border: 1px solid #333; + border-radius: 8px; + padding: 2rem; + margin-bottom: 2rem; +} + +.section h3 { + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.wallet-connect .button { + background: #3b82f6; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + cursor: pointer; + font-size: 1rem; + transition: background 0.2s; +} + +.wallet-connect .button:hover { + background: #2563eb; +} + +.wallet-connect .button:disabled { + background: #555; + cursor: not-allowed; +} + +.wallet-connect .connected { + display: flex; + align-items: center; + gap: 1rem; +} + +.wallet-connect .error { + color: #ef4444; + margin-bottom: 1rem; +} + +.token-balance { + text-align: center; +} + +.token-balance .balance { + font-size: 2rem; + font-weight: bold; + margin: 1rem 0; + color: #3b82f6; +} + +.token-balance .process-id { + font-size: 0.875rem; + color: #888; + margin-bottom: 1rem; +} + +.refresh-button { + background: #333; + color: white; + border: 1px solid #555; + padding: 0.5rem 1rem; + border-radius: 6px; + cursor: pointer; + font-size: 0.875rem; +} + +.refresh-button:hover { + background: #444; +} + +.send-tokens form { + max-width: 500px; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.875rem; + color: #888; +} + +.form-group input { + width: 100%; + padding: 0.75rem; + background: #222; + border: 1px solid #444; + border-radius: 6px; + color: white; + font-size: 1rem; +} + +.form-group input:focus { + outline: none; + border-color: #3b82f6; +} + +.form-group input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.send-tokens button[type="submit"] { + background: #3b82f6; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + cursor: pointer; + font-size: 1rem; +} + +.send-tokens button[type="submit"]:hover { + background: #2563eb; +} + +.send-tokens button[type="submit"]:disabled { + background: #555; + cursor: not-allowed; +} + +.send-tokens .error { + color: #ef4444; + margin-bottom: 1rem; +} + +.send-tokens .success { + color: #10b981; + margin-bottom: 1rem; +} + +.transaction-list .transactions { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 1rem; +} + +.transaction { + background: #1a1a1a; + border: 1px solid #333; + border-radius: 6px; + padding: 1rem; +} + +.transaction .tx-id a { + color: #3b82f6; + text-decoration: none; + font-family: monospace; +} + +.transaction .tx-id a:hover { + text-decoration: underline; +} + +.transaction .tx-time { + font-size: 0.875rem; + color: #888; + margin: 0.5rem 0; +} + +.transaction .tx-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.transaction .tag { + background: #222; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + color: #aaa; +} + +.loading { + text-align: center; + padding: 2rem; + color: #888; +} + +.error { + text-align: center; + padding: 2rem; + color: #ef4444; +} + +.empty { + text-align: center; + padding: 2rem; + color: #888; +} + +.connect-prompt { + text-align: center; + padding: 4rem 2rem; + color: #888; +} + +.app-footer { + padding: 2rem; + text-align: center; + background: #111; + border-top: 1px solid #333; + color: #666; +} + +@media (max-width: 768px) { + .app-header { + flex-direction: column; + gap: 1rem; + } + + .app-main { + padding: 1rem; + } + + .section { + padding: 1rem; + } +} +``` + +## Configuring Vite for SPA Routing + +For single-page application routing to work correctly on Arweave, you need to configure Vite properly. + +Update `vite.config.ts`: + +```typescript +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', + build: { + outDir: 'dist', + assetsDir: 'assets', + rollupOptions: { + output: { + manualChunks: undefined, + }, + }, + }, +}); +``` + + +The `base: './'` configuration ensures that all asset paths are relative, which is required for Arweave deployment. + + +## Testing Your dApp Locally + +Before deploying, test your dApp locally: + +```bash +npm run dev +``` + +Visit `http://localhost:5173` and test: + +1. **Wallet Connection**: Connect your Wander wallet +2. **Token Balance**: Verify your ARIO balance loads correctly +3. **Send Tokens**: Test sending ARIO tokens to another address +4. **Transaction List**: Check that recent transactions display + + +Make sure you have: +- Wander extension installed +- ARIO tokens in your wallet for testing transfers (the example uses the ARIO process ID) + + +## Building for Production + +Build your dApp for deployment: + +```bash +npm run build +``` + +This creates an optimized production build in the `dist/` directory. + +## Deploying to Arweave with permaweb-deploy + +Now let's deploy your AO dApp to Arweave with ArNS integration. + +### Interactive Deployment + + +You'll need a deployment wallet separate from your Wander wallet. This wallet should have storage credits from [turbo.ar.io](https://turbo.ar.io/topup). + + +Run the interactive deployment: + +```bash +npx permaweb-deploy +``` + +The CLI will prompt you for: +1. **Wallet path**: Path to your deployment wallet JSON file (e.g., `~/wallets/arweave-wallet.json`) +2. **Deploy folder**: Enter `dist` +3. **ArNS name**: Enter your registered ArNS name (e.g., `my-ao-dapp`) +4. **Undername**: Press Enter to deploy to root (`@`) +5. **TTL**: Enter `60` for fast updates (1 minute cache) + +### CLI Deployment + +For non-interactive deployment: + +```bash +npx permaweb-deploy \ + --deploy-folder dist \ + --wallet ~/wallets/arweave-wallet.json \ + --arns-name my-ao-dapp \ + --ttl 60 +``` + +After deployment, your dApp will be accessible at: +- `https://my-ao-dapp.arweave.net` +- `https://my-ao-dapp.g8way.io` +- `https://my-ao-dapp.ar-io.dev` +- And 100+ other AR.IO gateways! + +## Automating Deployments + +### Package Scripts + +Add deployment scripts to `package.json`: + +```json +{ + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "deploy": "npm run build && permaweb-deploy --deploy-folder dist --wallet \"$WALLET_PATH\" --arns-name \"$ARNS_NAME\" --ttl 60", + "deploy:dev": "npm run build && permaweb-deploy --deploy-folder dist --wallet \"$WALLET_PATH\" --arns-name \"$ARNS_NAME\" --undername dev_$ARNS_NAME --ttl 60" + } +} +``` + +Deploy with environment variables: + +```bash +WALLET_PATH=~/wallets/arweave-wallet.json ARNS_NAME=my-ao-dapp npm run deploy +``` + +### GitHub Actions + +Automate deployments on push to main branch. + +Create `.github/workflows/deploy.yml`: + +```yaml +name: Deploy AO dApp + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build + + - name: Create wallet file + run: | + mkdir -p ~/.wallets + echo '${{ secrets.DEPLOY_KEY }}' > ~/.wallets/deploy-wallet.json + + - name: Deploy to Arweave + run: | + npx permaweb-deploy \ + --deploy-folder dist \ + --wallet ~/.wallets/deploy-wallet.json \ + --arns-name ${{ secrets.ARNS_NAME }} \ + --ttl 60 + + - name: Clean up wallet + if: always() + run: rm -f ~/.wallets/deploy-wallet.json +``` + +#### Setting up GitHub Secrets + +Add these secrets to your repository (Settings → Secrets and variables → Actions): + +1. **DEPLOY_KEY**: Your deployment wallet JSON content (entire file content) +2. **ARNS_NAME**: Your ArNS name (e.g., `my-ao-dapp`) + + +Never commit wallet files or secrets to your repository. Always use GitHub Secrets or environment variables. + + +### Multi-Environment Deployments + +Deploy different branches to different undernames: + +```yaml +name: Deploy to Environments + +on: + push: + branches: + - main + - develop + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build + + - name: Create wallet file + run: | + mkdir -p ~/.wallets + echo '${{ secrets.DEPLOY_KEY }}' > ~/.wallets/deploy-wallet.json + + - name: Determine undername + id: undername + run: | + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + echo "undername=@" >> $GITHUB_OUTPUT + else + echo "undername=dev_${{ secrets.ARNS_NAME }}" >> $GITHUB_OUTPUT + fi + + - name: Deploy to Arweave + run: | + if [ "${{ steps.undername.outputs.undername }}" = "@" ]; then + npx permaweb-deploy \ + --deploy-folder dist \ + --wallet ~/.wallets/deploy-wallet.json \ + --arns-name ${{ secrets.ARNS_NAME }} \ + --ttl 60 + else + npx permaweb-deploy \ + --deploy-folder dist \ + --wallet ~/.wallets/deploy-wallet.json \ + --arns-name ${{ secrets.ARNS_NAME }} \ + --undername ${{ steps.undername.outputs.undername }} \ + --ttl 60 + fi + + - name: Display deployment URLs + run: | + if [ "${{ steps.undername.outputs.undername }}" = "@" ]; then + echo "Production deployed to:" + echo "- https://${{ secrets.ARNS_NAME }}.arweave.net" + else + echo "Development deployed to:" + echo "- https://${{ steps.undername.outputs.undername }}.arweave.net" + fi + + - name: Clean up wallet + if: always() + run: rm -f ~/.wallets/deploy-wallet.json +``` + +This workflow: +- Deploys `main` branch to root domain (`@`) +- Deploys `develop` branch to `dev_myapp` undername +- Works with any branch by adapting the undername + +## Next Steps + +Congratulations! You've built and deployed an ARIO token dashboard on Arweave. You can adapt this pattern to work with any AO token by changing the process ID. Here's what to explore next: + + + + + + + diff --git a/content/build/guides/hosting-unstoppable-apps/hosting-evm-dapp.mdx b/content/build/guides/hosting-unstoppable-apps/hosting-evm-dapp.mdx new file mode 100644 index 000000000..68fc68298 --- /dev/null +++ b/content/build/guides/hosting-unstoppable-apps/hosting-evm-dapp.mdx @@ -0,0 +1,2154 @@ +--- +title: "Hosting an EVM App on ArNS" +description: "Deploy an Ethereum dApp frontend to Arweave with ArNS integration, combining EVM smart contracts with permanent decentralized hosting" +--- + +import { Callout } from 'fumadocs-ui/components/callout'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Card, Cards } from 'fumadocs-ui/components/card'; +import { Code, Wallet, Coins, Network, GitBranch, Terminal, FileCode, Globe } from 'lucide-react'; + +## Introduction + +This guide shows you how to build and deploy an Ethereum dApp frontend to Arweave using ArNS. You'll create a complete ETH wallet dashboard with balance checking and transfer functionality, then host it permanently on Arweave while your smart contract logic runs on Ethereum. + +This **hybrid architecture** combines the best of both worlds: +- **EVM chains** (Ethereum, Base, etc.) for smart contract execution and state management +- **Arweave + ArNS** for permanent, censorship-resistant frontend hosting + +By the end of this guide, you'll have a production-ready dApp that users can access through your ArNS name, with the frontend stored permanently on Arweave and transactions executing on Ethereum or Base. + + +Check out the [series introduction](/build/guides/hosting-unstoppable-apps) to understand how AR.IO Network makes apps truly unstoppable with 100+ independent gateways. + + +## What You'll Build + +You'll create an **ETH Wallet Dashboard** that demonstrates core dApp functionality: + +- **Multi-Wallet Support**: Connect with MetaMask, Coinbase Wallet, Brave Wallet, and other browser extension wallets +- **Multi-Chain**: Support both Ethereum Mainnet and Base +- **View Balance**: Display user's ETH balance on connected chain +- **Send ETH**: Transfer ETH to other addresses with gas estimation +- **Network Switching**: Let users switch between Ethereum and Base +- **Permanent Hosting**: Deploy to Arweave with your ArNS domain + +The complete app will be ~350 lines of code across 5 components, fully typed with TypeScript. + +## Prerequisites + +Before starting, ensure you have: + +- **Node.js 18+** installed +- **Ethereum Wallet** (MetaMask, Coinbase Wallet, or similar) - You'll use this for both testing your dApp AND deploying to Arweave +- **ETH on Base** for Arweave storage payments (or Turbo credits from [turbo.ar.io/topup](https://turbo.ar.io/topup)) +- **ArNS Name** configured and ready ([purchase](https://arns.app)) +- Basic understanding of React and Ethereum + + +This guide uses your Ethereum wallet (MetaMask, etc.) for both: +- **Testing your dApp** - Connect to your dashboard, view balance, send transactions +- **Deploying to Arweave** - Use your wallet's private key with `permaweb-deploy` + +You can pay for Arweave storage with Base-ETH tokens directly from your Ethereum wallet, making this a fully EVM-native workflow. + + +## Project Setup + + + + +### Create React + Vite Project + +Vite is the recommended approach for EVM dApps due to its fast build times and excellent developer experience. + +```bash +npm create vite@latest my-eth-dashboard -- --template react-ts +cd my-eth-dashboard +``` + +### Install Dependencies + +Install the Web3 stack (wagmi + viem) and Arweave deployment tool: + +```bash +# Web3 libraries +npm install wagmi viem@2.x @tanstack/react-query + +# Deployment tool +npm install --save-dev permaweb-deploy +``` + +**Package versions:** +- `wagmi` - Ethereum interaction hooks (latest v2) +- `viem@2.x` - Ethereum client (wagmi v2 requires viem v2) +- `@tanstack/react-query` - Required by wagmi for data fetching + + +This guide uses wagmi with custom UI components and built-in connectors only. No WalletConnect project ID or third-party wallet libraries needed! + + + + + +### Create Next.js Project + +Next.js is great if you need server-side rendering or API routes, though this example will be client-side only. + +```bash +npx create-next-app@latest my-eth-dashboard --typescript --tailwind --app +cd my-eth-dashboard +``` + +Choose these options: +- ✅ TypeScript +- ✅ ESLint +- ✅ Tailwind CSS +- ✅ `src/` directory +- ✅ App Router +- ❌ Customize default import alias + +### Install Dependencies + +```bash +# Web3 libraries +npm install wagmi viem@2.x @tanstack/react-query + +# Deployment tool +npm install --save-dev permaweb-deploy +``` + + + + +## Configure Web3 Stack + +Now set up wagmi with custom connectors to handle wallet connections and Ethereum interactions. + + + + +### Create Web3 Configuration + +Create `src/wagmi.config.ts`: + +```typescript title="src/wagmi.config.ts" +import { http, createConfig } from 'wagmi'; +import { mainnet, base } from 'wagmi/chains'; +import { injected, metaMask, coinbaseWallet } from 'wagmi/connectors'; + +// Configure wagmi with injected wallet connectors only +export const config = createConfig({ + chains: [mainnet, base], + connectors: [ + injected(), // Auto-detects MetaMask, Brave, etc. + metaMask(), // Explicitly for MetaMask + coinbaseWallet({ + appName: 'ETH Dashboard', + }), + ], + transports: { + [mainnet.id]: http(), + [base.id]: http(), + }, +}); +``` + +**Supported wallets:** +- MetaMask (browser extension) +- Coinbase Wallet (browser extension) +- Brave Wallet (built-in to Brave browser) +- Any wallet that injects `window.ethereum` + + +This configuration uses only browser-injected wallets. No WalletConnect project ID needed! Mobile users can access via MetaMask mobile browser or Coinbase Wallet mobile browser. + + +### Setup Providers + +Update `src/main.tsx` to wrap your app with Web3 providers: + +```typescript title="src/main.tsx" +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +// Web3 imports +import { WagmiProvider } from 'wagmi'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { config } from './wagmi.config'; + +// Create React Query client +const queryClient = new QueryClient(); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + +); +``` + + + + +### Create Web3 Configuration + +Create `src/wagmi.config.ts`: + +```typescript title="src/wagmi.config.ts" +import { http, createConfig } from 'wagmi'; +import { mainnet, base } from 'wagmi/chains'; +import { injected, metaMask, coinbaseWallet } from 'wagmi/connectors'; + +export const config = createConfig({ + chains: [mainnet, base], + connectors: [ + injected(), + metaMask(), + coinbaseWallet({ + appName: 'ETH Dashboard', + }), + ], + transports: { + [mainnet.id]: http(), + [base.id]: http(), + }, + ssr: true, +}); +``` + +### Create Providers Component + +Create `src/providers/Web3Provider.tsx`: + +```typescript title="src/providers/Web3Provider.tsx" +'use client'; + +import { WagmiProvider } from 'wagmi'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { config } from '../wagmi.config'; + +const queryClient = new QueryClient(); + +export function Web3Provider({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} +``` + +### Setup Root Layout + +Update `src/app/layout.tsx`: + +```typescript title="src/app/layout.tsx" +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import './globals.css'; +import { Web3Provider } from '@/providers/Web3Provider'; + +const inter = Inter({ subsets: ['latin'] }); + +export const metadata: Metadata = { + title: 'ETH Dashboard', + description: 'Ethereum wallet dashboard on Arweave', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + + ); +} +``` + + + + +## Build the Components + +Now create the dashboard components. Each component demonstrates a key dApp pattern. + +### Component 1: Wallet Connection + +Create a custom wallet connection component that detects and connects to available wallets. + + + + +Create `src/components/WalletConnect.tsx`: + +```typescript title="src/components/WalletConnect.tsx" +import { useConnect, useAccount, useDisconnect, useChainId, useSwitchChain } from 'wagmi'; +import { mainnet, base } from 'wagmi/chains'; + +export function WalletConnect() { + const { connectors, connect, status, error } = useConnect(); + const { isConnected, address, chain } = useAccount(); + const { disconnect } = useDisconnect(); + const chainId = useChainId(); + const { switchChain } = useSwitchChain(); + + // Filter out duplicate connectors + const availableConnectors = connectors.filter( + (connector, index, self) => + index === self.findIndex((c) => c.name === connector.name) + ); + + if (isConnected) { + return ( +
+
+
+ {address?.slice(0, 6)}...{address?.slice(-4)} +
+
+ {chain?.name || 'Unknown Network'} +
+
+ +
+ {/* Network Switcher */} +
+ + +
+ + +
+
+ ); + } + + return ( +
+

Connect Your Wallet

+
+ {availableConnectors.map((connector) => ( + + ))} +
+ {error && ( +
+ Error: {error.message} +
+ )} +
+ ); +} +``` + +**What this component does:** +- Lists all available wallet connectors (MetaMask, Coinbase Wallet, etc.) +- Connects to selected wallet on click +- Shows connected address and network +- Provides network switcher between Ethereum and Base +- Handles disconnect functionality +- Displays connection errors + +
+ + +Create `src/components/WalletConnect.tsx`: + +```typescript title="src/components/WalletConnect.tsx" +'use client'; + +import { useConnect, useAccount, useDisconnect, useChainId, useSwitchChain } from 'wagmi'; +import { mainnet, base } from 'wagmi/chains'; + +export function WalletConnect() { + const { connectors, connect, status, error } = useConnect(); + const { isConnected, address, chain } = useAccount(); + const { disconnect } = useDisconnect(); + const chainId = useChainId(); + const { switchChain } = useSwitchChain(); + + const availableConnectors = connectors.filter( + (connector, index, self) => + index === self.findIndex((c) => c.name === connector.name) + ); + + if (isConnected) { + return ( +
+
+
+ {address?.slice(0, 6)}...{address?.slice(-4)} +
+
+ {chain?.name || 'Unknown Network'} +
+
+ +
+
+ + +
+ + +
+
+ ); + } + + return ( +
+

Connect Your Wallet

+
+ {availableConnectors.map((connector) => ( + + ))} +
+ {error && ( +
+ Error: {error.message} +
+ )} +
+ ); +} +``` + + +Next.js App Router requires `'use client'` for components that use hooks or browser APIs. + + +
+
+ +### Component 2: ETH Balance Display + +Display the user's ETH balance using wagmi's `useAccount` and `useBalance` hooks. + + + + +Create `src/components/EthBalance.tsx`: + +```typescript title="src/components/EthBalance.tsx" +import { useAccount, useBalance } from 'wagmi'; +import { formatEther } from 'viem'; + +export function EthBalance() { + const { address, isConnected, chain } = useAccount(); + + // Fetch ETH balance for connected address + const { data: balance, isError, isLoading } = useBalance({ + address: address, + }); + + if (!isConnected) { + return ( +
+

💰 Your Balance

+

Connect your wallet to view your balance

+
+ ); + } + + if (isLoading) { + return ( +
+

💰 Your Balance

+

Loading balance...

+
+ ); + } + + if (isError || !balance) { + return ( +
+

💰 Your Balance

+

Failed to load balance

+
+ ); + } + + return ( +
+

💰 Your Balance

+
+ {parseFloat(formatEther(balance.value)).toFixed(4)} {balance.symbol} +
+
+ {chain?.name} + + {address?.slice(0, 6)}...{address?.slice(-4)} + +
+
+ ); +} +``` + +**Key concepts:** +- `useAccount()` - Get connected wallet address and chain +- `useBalance()` - Fetch ETH balance (auto-updates on transactions) +- `formatEther()` - Convert wei (smallest unit) to ETH for display + +
+ + +Create `src/components/EthBalance.tsx`: + +```typescript title="src/components/EthBalance.tsx" +'use client'; + +import { useAccount, useBalance } from 'wagmi'; +import { formatEther } from 'viem'; + +export function EthBalance() { + const { address, isConnected, chain } = useAccount(); + + const { data: balance, isError, isLoading } = useBalance({ + address: address, + }); + + if (!isConnected) { + return ( +
+

💰 Your Balance

+

Connect your wallet to view your balance

+
+ ); + } + + if (isLoading) { + return ( +
+

💰 Your Balance

+

Loading balance...

+
+ ); + } + + if (isError || !balance) { + return ( +
+

💰 Your Balance

+

Failed to load balance

+
+ ); + } + + return ( +
+

💰 Your Balance

+
+ {parseFloat(formatEther(balance.value)).toFixed(4)} {balance.symbol} +
+
+ {chain?.name} + + {address?.slice(0, 6)}...{address?.slice(-4)} + +
+
+ ); +} +``` + +
+
+ +### Component 3: Send ETH + +Allow users to send ETH to other addresses with proper gas estimation and error handling. + + + + +Create `src/components/SendEth.tsx`: + +```typescript title="src/components/SendEth.tsx" +import { useState } from 'react'; +import { useAccount, useSendTransaction, useWaitForTransactionReceipt } from 'wagmi'; +import { parseEther } from 'viem'; + +export function SendEth() { + const { address, isConnected } = useAccount(); + const [recipient, setRecipient] = useState(''); + const [amount, setAmount] = useState(''); + + // Hook to send transaction + const { + data: hash, + isPending, + sendTransaction, + error + } = useSendTransaction(); + + // Hook to wait for confirmation + const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ + hash, + }); + + const handleSend = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!recipient || !amount) { + alert('Please enter recipient address and amount'); + return; + } + + try { + // Send ETH transaction + sendTransaction({ + to: recipient as `0x${string}`, + value: parseEther(amount), // Convert ETH to wei + }); + } catch (err) { + console.error('Transaction error:', err); + } + }; + + if (!isConnected) { + return ( +
+

📤 Send ETH

+

Connect your wallet to send ETH

+
+ ); + } + + return ( +
+

📤 Send ETH

+ +
+
+ + setRecipient(e.target.value)} + disabled={isPending || isConfirming} + /> +
+ +
+ + setAmount(e.target.value)} + disabled={isPending || isConfirming} + /> +
+ + +
+ + {error && ( +
+ Error: {error.message} +
+ )} + + {hash && ( +
+

Transaction Hash:

+ {hash} +
+ )} + + {isSuccess && ( +
+ ✅ Transaction confirmed! +
+ )} +
+ ); +} +``` + +**Key concepts:** +- `useSendTransaction()` - Send ETH transfers (triggers wallet approval) +- `useWaitForTransactionReceipt()` - Wait for transaction confirmation +- `parseEther()` - Convert ETH input to wei (1 ETH = 10^18 wei) +- Gas is estimated automatically by the wallet + +
+ + +Create `src/components/SendEth.tsx`: + +```typescript title="src/components/SendEth.tsx" +'use client'; + +import { useState } from 'react'; +import { useAccount, useSendTransaction, useWaitForTransactionReceipt } from 'wagmi'; +import { parseEther } from 'viem'; + +export function SendEth() { + const { address, isConnected } = useAccount(); + const [recipient, setRecipient] = useState(''); + const [amount, setAmount] = useState(''); + + const { + data: hash, + isPending, + sendTransaction, + error + } = useSendTransaction(); + + const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ + hash, + }); + + const handleSend = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!recipient || !amount) { + alert('Please enter recipient address and amount'); + return; + } + + try { + sendTransaction({ + to: recipient as `0x${string}`, + value: parseEther(amount), + }); + } catch (err) { + console.error('Transaction error:', err); + } + }; + + if (!isConnected) { + return ( +
+

📤 Send ETH

+

Connect your wallet to send ETH

+
+ ); + } + + return ( +
+

📤 Send ETH

+ +
+
+ + setRecipient(e.target.value)} + disabled={isPending || isConfirming} + /> +
+ +
+ + setAmount(e.target.value)} + disabled={isPending || isConfirming} + /> +
+ + +
+ + {error && ( +
+ Error: {error.message} +
+ )} + + {hash && ( +
+

Transaction Hash:

+ {hash} +
+ )} + + {isSuccess && ( +
+ ✅ Transaction confirmed! +
+ )} +
+ ); +} +``` + +
+
+ +## Create Main App + +Now assemble your components into the main application. + + + + +### Update App Component + +Replace `src/App.tsx` with: + +```typescript title="src/App.tsx" +import { WalletConnect } from './components/WalletConnect'; +import { EthBalance } from './components/EthBalance'; +import { SendEth } from './components/SendEth'; +import './App.css'; + +function App() { + return ( +
+
+

ETH Dashboard

+

Permanent hosting on Arweave

+
+ +
+ +
+ +
+ + +
+ + +
+ ); +} + +export default App; +``` + +### Add Styles + +Update `src/App.css`: + +```css title="src/App.css" +.app { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 2rem; +} + +.app-header { + text-align: center; + color: white; + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 3rem; + margin: 0; +} + +.subtitle { + font-size: 1.2rem; + opacity: 0.9; + margin-top: 0.5rem; +} + +.wallet-section { + display: flex; + justify-content: center; + margin-bottom: 2rem; +} + +/* Wallet Connection Styles */ +.wallet-connect, +.wallet-connected { + background: white; + border-radius: 16px; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + min-width: 400px; +} + +.wallet-connect h3 { + margin: 0 0 1.5rem 0; + color: #333; + text-align: center; +} + +.wallet-options { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.wallet-option { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem 1.5rem; + background: #f8f8f8; + border: 2px solid #e0e0e0; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s; + font-size: 1rem; + font-weight: 500; +} + +.wallet-option:hover:not(:disabled) { + background: #667eea; + border-color: #667eea; + color: white; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +.wallet-option:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.wallet-icon { + font-size: 1.5rem; +} + +.wallet-name { + font-weight: 600; +} + +.connect-error { + margin-top: 1rem; + padding: 0.75rem; + background: #fee; + border-left: 4px solid #f44; + border-radius: 4px; + color: #c33; + font-size: 0.9rem; +} + +/* Wallet Connected State */ +.wallet-connected { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.wallet-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding-bottom: 1rem; + border-bottom: 2px solid #e0e0e0; +} + +.wallet-address { + font-size: 1.2rem; + font-weight: 600; + color: #333; + font-family: 'Courier New', monospace; +} + +.wallet-network { + font-size: 0.9rem; + color: #666; + background: #f0f0f0; + padding: 0.25rem 0.75rem; + border-radius: 12px; +} + +.wallet-actions { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.network-switcher { + display: flex; + gap: 0.5rem; +} + +.network-button { + flex: 1; + padding: 0.75rem; + background: #f8f8f8; + border: 2px solid #e0e0e0; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; + font-weight: 500; +} + +.network-button:hover:not(:disabled) { + background: #e8e8f8; + border-color: #667eea; +} + +.network-button.active { + background: #667eea; + border-color: #667eea; + color: white; +} + +.network-button:disabled { + cursor: not-allowed; +} + +.disconnect-button { + width: 100%; + padding: 0.75rem; + background: #ff4444; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background 0.3s; + font-weight: 600; +} + +.disconnect-button:hover { + background: #cc0000; +} + +.dashboard-grid { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 2rem; +} + +.balance-card, +.send-card { + background: white; + border-radius: 12px; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} + +.balance-card h2, +.send-card h2 { + margin-top: 0; + color: #333; +} + +.balance-amount { + font-size: 2.5rem; + font-weight: bold; + color: #667eea; + margin: 1rem 0; +} + +.balance-details { + display: flex; + justify-content: space-between; + color: #666; + font-size: 0.9rem; +} + +.chain-name { + background: #667eea; + color: white; + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.8rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + color: #333; + font-weight: 500; +} + +.form-group input { + width: 100%; + padding: 0.75rem; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-size: 1rem; + transition: border-color 0.3s; +} + +.form-group input:focus { + outline: none; + border-color: #667eea; +} + +.form-group input:disabled { + background: #f5f5f5; + cursor: not-allowed; +} + +button[type="submit"] { + width: 100%; + padding: 1rem; + background: #667eea; + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: background 0.3s; +} + +button[type="submit"]:hover:not(:disabled) { + background: #5568d3; +} + +button[type="submit"]:disabled { + background: #ccc; + cursor: not-allowed; +} + +.error-message { + margin-top: 1rem; + padding: 1rem; + background: #fee; + border-left: 4px solid #f44; + border-radius: 4px; + color: #c33; +} + +.success-message { + margin-top: 1rem; + padding: 1rem; + background: #efe; + border-left: 4px solid #4c4; + border-radius: 4px; + color: #363; +} + +.transaction-hash { + margin-top: 1rem; + padding: 1rem; + background: #f5f5f5; + border-radius: 4px; +} + +.transaction-hash code { + word-break: break-all; + color: #667eea; +} + +.app-footer { + text-align: center; + margin-top: 3rem; + color: white; + opacity: 0.8; +} + +.app-footer a { + color: white; + text-decoration: underline; +} + +@media (max-width: 768px) { + .dashboard-grid { + grid-template-columns: 1fr; + } +} +``` + +Update `src/index.css` for base styles: + +```css title="src/index.css" +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} +``` + +
+ + +### Create Page Component + +Replace `src/app/page.tsx` with: + +```typescript title="src/app/page.tsx" +'use client'; + +import { WalletConnect } from '@/components/WalletConnect'; +import { EthBalance } from '@/components/EthBalance'; +import { SendEth } from '@/components/SendEth'; + +export default function Home() { + return ( +
+
+

ETH Dashboard

+

Permanent hosting on Arweave

+
+ +
+ +
+ +
+ + +
+ + +
+ ); +} +``` + +### Add Styles + +Update `src/app/globals.css`: + +```css title="src/app/globals.css" +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; +} + +.app { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 2rem; +} + +.app-header { + text-align: center; + color: white; + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 3rem; + margin: 0; +} + +.subtitle { + font-size: 1.2rem; + opacity: 0.9; + margin-top: 0.5rem; +} + +.wallet-section { + display: flex; + justify-content: center; + margin-bottom: 2rem; +} + +/* Wallet Connection Styles */ +.wallet-connect, +.wallet-connected { + background: white; + border-radius: 16px; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + min-width: 400px; +} + +.wallet-connect h3 { + margin: 0 0 1.5rem 0; + color: #333; + text-align: center; +} + +.wallet-options { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.wallet-option { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem 1.5rem; + background: #f8f8f8; + border: 2px solid #e0e0e0; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s; + font-size: 1rem; + font-weight: 500; +} + +.wallet-option:hover:not(:disabled) { + background: #667eea; + border-color: #667eea; + color: white; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +.wallet-option:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.wallet-icon { + font-size: 1.5rem; +} + +.wallet-name { + font-weight: 600; +} + +.connect-error { + margin-top: 1rem; + padding: 0.75rem; + background: #fee; + border-left: 4px solid #f44; + border-radius: 4px; + color: #c33; + font-size: 0.9rem; +} + +/* Wallet Connected State */ +.wallet-connected { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.wallet-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding-bottom: 1rem; + border-bottom: 2px solid #e0e0e0; +} + +.wallet-address { + font-size: 1.2rem; + font-weight: 600; + color: #333; + font-family: 'Courier New', monospace; +} + +.wallet-network { + font-size: 0.9rem; + color: #666; + background: #f0f0f0; + padding: 0.25rem 0.75rem; + border-radius: 12px; +} + +.wallet-actions { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.network-switcher { + display: flex; + gap: 0.5rem; +} + +.network-button { + flex: 1; + padding: 0.75rem; + background: #f8f8f8; + border: 2px solid #e0e0e0; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; + font-weight: 500; +} + +.network-button:hover:not(:disabled) { + background: #e8e8f8; + border-color: #667eea; +} + +.network-button.active { + background: #667eea; + border-color: #667eea; + color: white; +} + +.network-button:disabled { + cursor: not-allowed; +} + +.disconnect-button { + width: 100%; + padding: 0.75rem; + background: #ff4444; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background 0.3s; + font-weight: 600; +} + +.disconnect-button:hover { + background: #cc0000; +} + +.dashboard-grid { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 2rem; +} + +.balance-card, +.send-card { + background: white; + border-radius: 12px; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} + +.balance-card h2, +.send-card h2 { + margin-top: 0; + color: #333; +} + +.balance-amount { + font-size: 2.5rem; + font-weight: bold; + color: #667eea; + margin: 1rem 0; +} + +.balance-details { + display: flex; + justify-content: space-between; + color: #666; + font-size: 0.9rem; +} + +.chain-name { + background: #667eea; + color: white; + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.8rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + color: #333; + font-weight: 500; +} + +.form-group input { + width: 100%; + padding: 0.75rem; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-size: 1rem; + transition: border-color 0.3s; +} + +.form-group input:focus { + outline: none; + border-color: #667eea; +} + +.form-group input:disabled { + background: #f5f5f5; + cursor: not-allowed; +} + +button[type="submit"] { + width: 100%; + padding: 1rem; + background: #667eea; + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: background 0.3s; +} + +button[type="submit"]:hover:not(:disabled) { + background: #5568d3; +} + +button[type="submit"]:disabled { + background: #ccc; + cursor: not-allowed; +} + +.error-message { + margin-top: 1rem; + padding: 1rem; + background: #fee; + border-left: 4px solid #f44; + border-radius: 4px; + color: #c33; +} + +.success-message { + margin-top: 1rem; + padding: 1rem; + background: #efe; + border-left: 4px solid #4c4; + border-radius: 4px; + color: #363; +} + +.transaction-hash { + margin-top: 1rem; + padding: 1rem; + background: #f5f5f5; + border-radius: 4px; +} + +.transaction-hash code { + word-break: break-all; + color: #667eea; +} + +.app-footer { + text-align: center; + margin-top: 3rem; + color: white; + opacity: 0.8; +} + +.app-footer a { + color: white; + text-decoration: underline; +} + +@media (max-width: 768px) { + .dashboard-grid { + grid-template-columns: 1fr; + } +} +``` + +
+
+ +## Build Configuration + +Configure your framework to output a properly formatted SPA for Arweave. + + + + +### Configure Vite + +Update `vite.config.ts`: + +```typescript title="vite.config.ts" +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', // Use relative paths for Arweave + build: { + outDir: 'dist', + assetsDir: 'assets', + rollupOptions: { + output: { + manualChunks: undefined, // Single bundle for simplicity + }, + }, + }, +}); +``` + +### Build the Project + +```bash +npm run build +``` + +This creates a `dist/` folder with your compiled app ready for Arweave deployment. + + + + +### Configure Next.js for Static Export + +Update `next.config.mjs`: + +```javascript title="next.config.mjs" +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'export', // Enable static export + images: { + unoptimized: true, // Required for static export + }, + trailingSlash: true, // Add trailing slashes to URLs +}; + +export default nextConfig; +``` + + +`output: 'export'` generates a static site that works on Arweave. This disables server-side features like API routes and ISR. + + +### Build the Project + +```bash +npm run build +``` + +This creates an `out/` folder with your static site ready for deployment. + + + + +## Deploy to Arweave + +Now deploy your dApp frontend to Arweave using your Ethereum wallet. + +### Export Your Private Key + +You'll need your Ethereum wallet's private key for deployment. Here's how to export it from MetaMask: + + + + +Click the MetaMask extension and unlock your wallet. + + + +1. Click the three dots menu next to your account name +2. Select "Account details" + + + +1. Click "Show private key" +2. Enter your MetaMask password +3. Click "Confirm" to reveal your private key +4. Copy the private key (starts with `0x`) + + + + + +**NEVER share your private key or commit it to version control.** + +Your private key grants full access to your wallet funds. Only use it locally or in secure CI/CD environments like GitHub Secrets. Consider using a dedicated deployment wallet with limited funds for additional security. + + +### Payment Options + +You have two options to pay for Arweave storage: + +1. **Base-ETH** (Recommended for EVM users): Pay directly with ETH on Base network using `--on-demand base-eth` +2. **Turbo Credits**: Pre-purchase credits at [turbo.ar.io/topup](https://turbo.ar.io/topup) + + + + +### Deploy with Base-ETH (Recommended) + +Deploy and pay for storage using ETH on Base network: + +```bash +npx permaweb-deploy \ + --arns-name my-eth-dashboard \ + --sig-type ethereum \ + --private-key "0xYOUR_PRIVATE_KEY" \ + --on-demand base-eth \ + --max-token-amount 0.01 \ + --ttl 60 +``` + +**Options explained:** +- `--sig-type ethereum` - Use Ethereum wallet for signing +- `--private-key` - Your MetaMask private key (starts with `0x`) +- `--on-demand base-eth` - Pay with ETH on Base network +- `--max-token-amount 0.01` - Maximum ETH to spend (adjust as needed) +- `--ttl 60` - ArNS updates propagate within 1 minute + + +This app's production build is approximately **300 KB** (0.3 MB). Use the [Turbo Calculator](https://turbo.ar.io/calculator) to calculate the exact cost based on your build size. You'll need at least this equivalent in Base-ETH in your wallet (or Turbo credits) to cover the upload cost. + + +### Deploy with Turbo Credits (Alternative) + +If you prefer using pre-purchased Turbo credits: + +```bash +npx permaweb-deploy \ + --arns-name my-eth-dashboard \ + --sig-type ethereum \ + --private-key "0xYOUR_PRIVATE_KEY" \ + --ttl 60 +``` + +### Package.json Script + +Create a deployment script for easier local deploys: + +```json title="package.json" +{ + "scripts": { + "deploy": "permaweb-deploy --arns-name $ARNS_NAME --sig-type ethereum --private-key $PRIVATE_KEY --on-demand base-eth --max-token-amount 0.01 --ttl 60" + } +} +``` + +Then deploy with: + +```bash +ARNS_NAME=my-eth-dashboard PRIVATE_KEY="0x..." npm run deploy +``` + + + + +### Deploy with Base-ETH (Recommended) + +Deploy and pay for storage using ETH on Base network: + +```bash +npx permaweb-deploy \ + --deploy-folder out \ + --arns-name my-eth-dashboard \ + --sig-type ethereum \ + --private-key "0xYOUR_PRIVATE_KEY" \ + --on-demand base-eth \ + --max-token-amount 0.01 \ + --ttl 60 +``` + +**Options explained:** +- `--deploy-folder out` - Next.js static export folder +- `--sig-type ethereum` - Use Ethereum wallet for signing +- `--private-key` - Your MetaMask private key (starts with `0x`) +- `--on-demand base-eth` - Pay with ETH on Base network +- `--max-token-amount 0.01` - Maximum ETH to spend (adjust as needed) +- `--ttl 60` - ArNS updates propagate within 1 minute + +### Deploy with Turbo Credits (Alternative) + +If you prefer using pre-purchased Turbo credits: + +```bash +npx permaweb-deploy \ + --deploy-folder out \ + --arns-name my-eth-dashboard \ + --sig-type ethereum \ + --private-key "0xYOUR_PRIVATE_KEY" \ + --ttl 60 +``` + +### Package.json Script + +Create a deployment script: + +```json title="package.json" +{ + "scripts": { + "deploy": "permaweb-deploy --deploy-folder out --arns-name $ARNS_NAME --sig-type ethereum --private-key $PRIVATE_KEY --on-demand base-eth --max-token-amount 0.01 --ttl 60" + } +} +``` + +Then deploy with: + +```bash +ARNS_NAME=my-eth-dashboard PRIVATE_KEY="0x..." npm run deploy +``` + + + + +### Access Your dApp + +After deployment completes (1-2 minutes), access your dApp at: + +- **Primary**: `https://my-eth-dashboard.arweave.net` +- **Alternative gateways**: `https://my-eth-dashboard.g8way.io`, `https://my-eth-dashboard.ar-io.dev` + +Your dApp is now permanently hosted across 100+ AR.IO gateways! + +## Automating Deployments + +### GitHub Actions + +Set up CI/CD to automatically deploy when you push to GitHub. + + + + +Create `.github/workflows/deploy.yml`: + +```yaml title=".github/workflows/deploy.yml" +name: Deploy to Arweave + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Deploy to Arweave + env: + ARNS_NAME: ${{ secrets.ARNS_NAME }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + run: | + npx permaweb-deploy \ + --arns-name $ARNS_NAME \ + --sig-type ethereum \ + --private-key $PRIVATE_KEY \ + --on-demand base-eth \ + --max-token-amount 0.01 \ + --ttl 60 +``` + + + + +Create `.github/workflows/deploy.yml`: + +```yaml title=".github/workflows/deploy.yml" +name: Deploy to Arweave + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Deploy to Arweave + env: + ARNS_NAME: ${{ secrets.ARNS_NAME }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + run: | + npx permaweb-deploy \ + --deploy-folder out \ + --arns-name $ARNS_NAME \ + --sig-type ethereum \ + --private-key $PRIVATE_KEY \ + --on-demand base-eth \ + --max-token-amount 0.01 \ + --ttl 60 +``` + + + + +### Configure GitHub Secrets + + + + +Export your Ethereum wallet's private key from MetaMask (see "Export Your Private Key" section above). + + + +1. Go to your repository on GitHub +2. Navigate to Settings → Secrets and variables → Actions +3. Add these secrets: + - `PRIVATE_KEY`: Your Ethereum private key (starts with `0x`) + - `ARNS_NAME`: Your ArNS name (e.g., `my-eth-dashboard`) + + +For production deployments, consider creating a separate Ethereum wallet specifically for deployments. Fund it with just enough Base-ETH for deployments (e.g., $5-10 worth) to limit exposure if the secret is compromised. + + + + +Push a commit to the `main` branch and watch the Actions tab for deployment progress. + + + + +### Multi-Environment Setup + +Deploy to different ArNS undernames for staging and production: + +```yaml title=".github/workflows/deploy.yml" {8-12,36-38} +name: Deploy to Arweave + +on: + push: + branches: [main, develop] + workflow_dispatch: + +env: + ARNS_NAME: ${{ github.ref == 'refs/heads/main' && secrets.ARNS_NAME_PROD || secrets.ARNS_NAME_DEV }} + UNDERNAME: ${{ github.ref == 'refs/heads/main' && '@' || 'dev' }} + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Deploy to Arweave + env: + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + run: | + npx permaweb-deploy \ + --arns-name $ARNS_NAME \ + --undername $UNDERNAME \ + --sig-type ethereum \ + --private-key $PRIVATE_KEY \ + --on-demand base-eth \ + --max-token-amount 0.01 \ + --ttl 60 +``` + +This deploys: +- `main` branch → `myapp.arweave.net` (production) +- `develop` branch → `dev_myapp.arweave.net` (staging) + +## Test Your dApp + +Before sharing your dApp, verify all functionality works: + + + + +1. Open your deployed dApp (e.g., `https://my-eth-dashboard.arweave.net`) +2. Click "Connect Wallet" and approve in MetaMask/your wallet +3. Verify your address and balance display correctly + + + +1. Use the network switcher to toggle between Ethereum and Base +2. Verify balance updates for each network +3. Check that chain name displays correctly + + + + +Test your dApp on testnets (Sepolia, Base Sepolia) before using real ETH. Update the chain configuration to include testnets during development. + + +1. Send a small test transaction to another address you control +2. Verify the transaction hash displays +3. Check transaction confirmation works +4. Verify balance updates after confirmation + + + +1. Try sending with insufficient balance +2. Try rejecting a transaction in your wallet +3. Verify error messages display correctly + + + +1. Open your dApp on mobile +2. Test WalletConnect integration with mobile wallets +3. Verify responsive design works + + + + +## Next Steps + + + } + title="Versioning with Undernames" + href="/build/guides/hosting-unstoppable-apps/using-undernames-for-versioning" + > + Deploy staging and production versions of your dApp to different undernames for safe updates and testing. + + + } + title="AO dApp Alternative" + href="/build/guides/hosting-unstoppable-apps/hosting-arweave-ao-dapp" + > + Explore building fully decentralized dApps using AO smart contracts instead of EVM chains. + + + } + title="Quick Deploy with Arlink" + href="/build/guides/hosting-unstoppable-apps/deploying-with-arlink" + > + Use Arlink's web interface for one-click deployments without CLI tools or local builds. + + + +You've successfully built and deployed an Ethereum dApp frontend to Arweave! Your app now benefits from: + +- ✅ **Permanent hosting** - Frontend stored forever on Arweave +- ✅ **Decentralized access** - Available through 100+ AR.IO gateways +- ✅ **Human-readable domain** - Accessible via your ArNS name +- ✅ **Hybrid architecture** - Smart contracts on Ethereum, frontend on Arweave +- ✅ **Multi-chain support** - Works with Ethereum, Base, and other EVM chains +- ✅ **Automated deployment** - CI/CD pipeline for updates + +This architecture gives you the best of both worlds: the maturity and ecosystem of EVM chains for your contracts, with the permanence and censorship-resistance of Arweave for your frontend. diff --git a/content/build/guides/deploy-dapp-with-ardrive-web.mdx b/content/build/guides/hosting-unstoppable-apps/hosting-with-ardrive.mdx similarity index 77% rename from content/build/guides/deploy-dapp-with-ardrive-web.mdx rename to content/build/guides/hosting-unstoppable-apps/hosting-with-ardrive.mdx index fa8f05993..bd07c76fb 100644 --- a/content/build/guides/deploy-dapp-with-ardrive-web.mdx +++ b/content/build/guides/hosting-unstoppable-apps/hosting-with-ardrive.mdx @@ -1,10 +1,21 @@ --- -title: "Deploy a dApp with ArDrive Web" +title: "Deploying with ArDrive" description: "How to upload a dApp to the permaweb using ArDrive web" --- +import { Callout } from 'fumadocs-ui/components/callout'; +import { Steps, Step } from 'fumadocs-ui/components/steps'; +import { Card, Cards } from 'fumadocs-ui/components/card'; +import { Upload, Globe, Code } from 'lucide-react'; + +## Introduction + Create **permanent dApps** using the ArDrive web interface. This guide shows you how to deploy your dApp or website to the permaweb using ArDrive's user-friendly interface. + +Check out the [series introduction](/build/guides/hosting-unstoppable-apps) to understand how AR.IO Network makes apps truly unstoppable with 100+ independent gateways. + + ## What You'll Learn - How to deploy dApps using ArDrive web @@ -28,91 +39,67 @@ Create **permanent dApps** using the ArDrive web interface. This guide shows you ## Step-by-Step Deployment - - ### Log into ArDrive - + Go to the [ArDrive web app](https://app.ardrive.io/#/sign-in) and log in using your preferred method. If you don't have an account, follow the instructions to create one. - - - ### Select or Create a Drive - + Navigate to the drive where you want your project hosted. If you need a new drive: - Click the big red "New" button at the top left - Create a new drive - **Important:** Set the drive to **public** for others to access your dApp - - - ### Upload Your Project - + With your drive selected: - Click the big red "New" button again - Select "Upload Folder" - Navigate to your project's root directory (or built directory if required) - Select the entire directory to maintain your project's file structure - - - ### Confirm Upload - + Review the upload and associated cost. If everything looks correct, click "Confirm". **Cost Note:** Uploading to Arweave isn't free, but costs are usually quite small compared to the benefits of permanent hosting. - - - ### Create the Manifest - + While ArDrive displays files as a traditional file structure, they don't actually exist that way on Arweave. The manifest acts as a map to all your dApp files: - Navigate into your newly created folder by double-clicking it - Click the big red "New" button again - Select "New Manifest" in the "Advanced" section - Name the manifest and save it inside the folder you just created - - - ### Get the Data TX ID - + Once the manifest is created: - Click on it to expand its details - Go to the "Details" tab - Find the "Data TX ID" on the bottom right - Copy this unique identifier for your dApp - - - ### View and Share Your dApp - + Your dApp is now live on the permaweb forever! - Append the Data TX ID to an Arweave gateway URL: `https://arweave.net/YOUR-TX-ID` - It may take a few minutes for files to propagate through the network - Once propagated, your dApp is accessible to anyone, anywhere, at any time - - - ### Assign a Friendly Name (Optional) - + Make your dApp easier to access with an ArNS name: - If you own an ArNS name, you'll be prompted during manifest creation - If not, purchase one from [arns.app](https://arns.arweave.net) - You can also assign an ArNS name later by clicking the three dots next to any file and selecting "Assign ArNS name" - @@ -144,31 +131,25 @@ Files uploaded to Arweave are **permanent and immutable** - they cannot be chang - **Version management** - Built-in file history and updates - **Cost transparency** - See upload costs before confirming -## Ready to Deploy? +## Next Steps } - arrow - > - Deploy your dApp using the ArDrive web interface - - -} -> - Learn how to create friendly domain names for your dApp - - + /> + } + /> } - > - Explore more advanced deployment options and tools - + /> diff --git a/content/build/guides/hosting-unstoppable-apps/index.mdx b/content/build/guides/hosting-unstoppable-apps/index.mdx new file mode 100644 index 000000000..0abaf52e9 --- /dev/null +++ b/content/build/guides/hosting-unstoppable-apps/index.mdx @@ -0,0 +1,81 @@ +--- +title: "Hosting Unstoppable Apps on ArNS" +description: "Learn how to deploy permanent, censorship-resistant websites and applications to Arweave with ArNS domain integration" +--- + +import { + BookOpen, + Rocket, + Code, + GitBranch, + Globe, + Zap, + Upload, +} from "lucide-react"; + +## Introduction + +Unstoppable apps are **permanent, censorship-resistant applications** deployed to Arweave's permanent storage network and made accessible through ArNS (Arweave Name System) domains. Unlike traditional web hosting that requires ongoing server maintenance and can be taken down, unstoppable apps are: + +- **Permanent** - Your application lives forever on Arweave +- **Censorship-resistant** - No single entity can remove or modify your content +- **Decentralized** - No central servers or hosting companies required +- **Cost-effective** - Pay once to store your application forever +- **Human-readable** - Access your app via friendly ArNS names like `myapp.arweave.net` + +This has always been the goal of full stack decentralised apps (dapps) but until now that has not been possible due to single points of failure for hosting frontends. AR.IO Network and Arweave Name System (ArNS) has solved this problem. + +## How AR.IO Network Makes Apps Unstoppable + +ArNS domains resolve across the entire AR.IO Network of 100+ independent gateways. This means your app at `myapp.arweave.net` is simultaneously accessible from: + +- `myapp.arweave.net` +- `myapp.ar.io` +- ...and 100+ more gateway domains + +Exactly the same app, different gateway. + +This creates true redundancy - if one gateway is blocked or goes down, your app remains accessible through hundreds of others. No single point of failure exists. + +In this guide series, you will learn how to deploy static websites, blogs, Arweave/AO dApps, and EVM dApps to create truly unstoppable applications. + +## Guides + + + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + diff --git a/content/build/guides/hosting-unstoppable-apps/meta.json b/content/build/guides/hosting-unstoppable-apps/meta.json new file mode 100644 index 000000000..cf26d9053 --- /dev/null +++ b/content/build/guides/hosting-unstoppable-apps/meta.json @@ -0,0 +1,12 @@ +{ + "title": "Hosting Unstoppable Apps", + "defaultOpen": false, + "pages": [ + "hosting-a-blog", + "hosting-arweave-ao-dapp", + "hosting-evm-dapp", + "using-undernames-for-versioning", + "deploying-with-arlink", + "hosting-with-ardrive" + ] +} diff --git a/content/build/guides/hosting-unstoppable-apps/using-undernames-for-versioning.mdx b/content/build/guides/hosting-unstoppable-apps/using-undernames-for-versioning.mdx new file mode 100644 index 000000000..4c4f1cc72 --- /dev/null +++ b/content/build/guides/hosting-unstoppable-apps/using-undernames-for-versioning.mdx @@ -0,0 +1,804 @@ +--- +title: "Using Undernames for Versioning" +description: "Learn practical versioning patterns using ArNS undernames for managing multiple versions and environments of your unstoppable apps" +--- + +import { Callout } from 'fumadocs-ui/components/callout'; +import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Card, Cards } from 'fumadocs-ui/components/card'; +import { GitBranch, Code, Rocket, Network, Settings, RefreshCw, CheckCircle } from 'lucide-react'; + +## Introduction + + +Check out the [series introduction](/build/guides/hosting-unstoppable-apps) to understand how AR.IO Network makes apps truly unstoppable with 100+ independent gateways. + + +While the previous guides showed you how to deploy with the `--undername` flag, this guide teaches you **strategic versioning patterns** for managing multiple versions and environments of your application. + +## What You'll Learn + +- **Understanding undernames** as sub-domains under your ArNS name +- **Common versioning patterns** (v1/v2/v3, staging/production, component architecture) +- **Environment workflows** for dev → staging → production +- **Automation strategies** with package.json scripts and GitHub Actions +- **Managing undernames** via the ArNS app interface +- **Best practices** for choosing and implementing versioning strategies + +## Prerequisites + +This guide assumes you've completed at least one of the deployment guides and are familiar with basic `permaweb-deploy` usage: + +- [Hosting a Blog](/build/guides/hosting-unstoppable-apps/hosting-a-blog) +- [Hosting an Arweave/AO dApp](/build/guides/hosting-unstoppable-apps/hosting-arweave-ao-dapp) +- [Hosting an EVM dApp](/build/guides/hosting-unstoppable-apps/hosting-evm-dapp) + +You should understand how to deploy with the `--undername` flag before exploring advanced versioning strategies. + +## Understanding Undernames + +**Undernames** are sub-domains under your main ArNS name, similar to traditional DNS subdomains but permanent and decentralized. + +### How Undernames Work + +``` +Your ArNS Name: myapp + +├─ @ (base record) → myapp.arweave.net +├─ staging → staging_myapp.arweave.net +├─ dev → dev_myapp.arweave.net +├─ v1 → v1_myapp.arweave.net +└─ v2 → v2_myapp.arweave.net +``` + +**Key Points:** + +- **One ArNS name** → Unlimited undernames (no extra cost) +- **Each undername** → Points to a different Arweave transaction ID +- **Permanent history** → All versions remain accessible forever +- **Naming convention** → Undernames use underscores in URLs (e.g., `staging_myapp.arweave.net`) + +### Why Use Undernames? + +**Instead of buying multiple ArNS names:** + +❌ `myapp-staging.arweave.net` (separate name, extra cost) +❌ `myapp-v2.arweave.net` (separate name, extra cost) + +✅ `staging_myapp.arweave.net` (one name, unlimited undernames) +✅ `v2_myapp.arweave.net` (one name, unlimited undernames) + +**Benefits:** + +- **Cost-effective** - One registration fee, unlimited versions +- **Organized** - Logical structure for versions and environments +- **Permanent** - All versions accessible forever +- **Transferable** - Undernames transfer with the main ArNS name +- **Instant rollbacks** - Via the ArNS app interface + +--- + +## Common Versioning Patterns + +### Pattern 1: Environment-Based Versioning + +Best for: Most production applications + +**Structure:** + +``` +@ (production) → Current production release +staging → Pre-release testing environment +dev → Active development builds +``` + +**Deployment workflow:** + + + + + +```bash +# Deploy latest changes to dev for testing +permaweb-deploy --arns-name myapp --undername dev --ttl 60 +``` + +Access at: `https://dev_myapp.arweave.net` + + + + + +```bash +# Once dev is tested, deploy to staging +permaweb-deploy --arns-name myapp --undername staging --ttl 60 +``` + +Access at: `https://staging_myapp.arweave.net` + + + + + +```bash +# After staging approval, deploy to production +permaweb-deploy --arns-name myapp --ttl 60 +``` + +Access at: `https://myapp.arweave.net` + + + + + +**Package.json scripts:** + +```json title="package.json" +{ + "scripts": { + "deploy:dev": "permaweb-deploy --arns-name myapp --undername dev --ttl 60", + "deploy:staging": "permaweb-deploy --arns-name myapp --undername staging --ttl 60", + "deploy:prod": "permaweb-deploy --arns-name myapp --ttl 60" + } +} +``` + +**Usage:** + +```bash +npm run deploy:dev # Deploy to dev +npm run deploy:staging # Deploy to staging +npm run deploy:prod # Deploy to production +``` + +--- + +### Pattern 2: Version Number + Environment + +Best for: Applications needing version history and environments + +**Structure:** + +``` +@ (production) → v2.0 (current release) +staging → v2.1 (next release testing) +dev → Latest development +v1 → Version 1.x (legacy, archived) +v2 → Version 2.x (current) +``` + +**Deployment workflow:** + +```bash +# Development +permaweb-deploy --arns-name myapp --undername dev --ttl 60 + +# Staging for v2.1 +permaweb-deploy --arns-name myapp --undername staging --ttl 60 + +# Release v2.1 to production +permaweb-deploy --arns-name myapp --ttl 60 + +# Archive v2.1 for permanent access +permaweb-deploy --arns-name myapp --undername v2-1 --ttl 3600 +``` + +**Package.json scripts:** + +```json title="package.json" +{ + "scripts": { + "deploy:dev": "permaweb-deploy --arns-name myapp --undername dev --ttl 60", + "deploy:staging": "permaweb-deploy --arns-name myapp --undername staging --ttl 60", + "deploy:prod": "permaweb-deploy --arns-name myapp --ttl 60", + "deploy:archive": "node scripts/deploy-archive.js" + } +} +``` + +**Archive script:** + +```javascript title="scripts/deploy-archive.js" +const { execSync } = require('child_process'); +const packageJson = require('../package.json'); + +// Get version from package.json (e.g., "2.1.0") +const version = packageJson.version.replace(/\./g, '-'); // "2-1-0" + +console.log(`Archiving version ${version}...`); + +// Deploy to version-specific undername +execSync( + `permaweb-deploy --arns-name myapp --undername v${version} --ttl 3600`, + { stdio: 'inherit' } +); + +console.log(`✓ Archived at: https://v${version}_myapp.arweave.net`); +``` + +--- + +### Pattern 3: Component Architecture + +Best for: Large applications with separate parts + +**Structure:** + +``` +@ (marketing) → Marketing website +app → Main application +api → API documentation +docs → User documentation +admin → Admin panel +``` + +**Deployment commands:** + +```bash +# Deploy each component independently +permaweb-deploy --arns-name myapp --deploy-folder ./marketing/dist --ttl 3600 +permaweb-deploy --arns-name myapp --undername app --deploy-folder ./app/dist --ttl 60 +permaweb-deploy --arns-name myapp --undername api --deploy-folder ./api-docs/dist --ttl 1800 +permaweb-deploy --arns-name myapp --undername docs --deploy-folder ./docs/dist --ttl 1800 +permaweb-deploy --arns-name myapp --undername admin --deploy-folder ./admin/dist --ttl 60 +``` + +**Package.json scripts:** + +```json title="package.json" +{ + "scripts": { + "deploy:marketing": "permaweb-deploy --arns-name myapp --deploy-folder ./marketing/dist --ttl 3600", + "deploy:app": "permaweb-deploy --arns-name myapp --undername app --deploy-folder ./app/dist --ttl 60", + "deploy:api": "permaweb-deploy --arns-name myapp --undername api --deploy-folder ./api-docs/dist --ttl 1800", + "deploy:docs": "permaweb-deploy --arns-name myapp --undername docs --deploy-folder ./docs/dist --ttl 1800", + "deploy:admin": "permaweb-deploy --arns-name myapp --undername admin --deploy-folder ./admin/dist --ttl 60", + "deploy:all": "npm run deploy:marketing && npm run deploy:app && npm run deploy:api && npm run deploy:docs && npm run deploy:admin" + } +} +``` + +--- + +## GitHub Actions Workflows + +### Multi-Environment Pipeline + +Deploy automatically based on branch: + +```yaml title=".github/workflows/deploy.yml" +name: Deploy to Environments + +on: + push: + branches: + - develop # Deploys to dev + - main # Deploys to staging + +jobs: + deploy-dev: + if: github.ref == 'refs/heads/develop' + runs-on: ubuntu-latest + environment: development + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install & Build + run: | + npm ci + npm run build + + - name: Deploy to Dev + env: + DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} + ARNS_NAME: ${{ secrets.ARNS_NAME }} + run: | + npx permaweb-deploy \ + --arns-name $ARNS_NAME \ + --undername dev \ + --wallet-file <(echo "$DEPLOY_KEY") \ + --ttl 60 + + - name: Comment on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `🚀 Deployed to dev: https://dev_${{ secrets.ARNS_NAME }}.arweave.net` + }) + + deploy-staging: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: staging + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install & Build + run: | + npm ci + npm run build + + - name: Deploy to Staging + env: + DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} + ARNS_NAME: ${{ secrets.ARNS_NAME }} + run: | + npx permaweb-deploy \ + --arns-name $ARNS_NAME \ + --undername staging \ + --wallet-file <(echo "$DEPLOY_KEY") \ + --ttl 60 + + - name: Run Integration Tests + run: npm run test:integration -- --base-url=https://staging_${{ secrets.ARNS_NAME }}.arweave.net + + - name: Notify Staging Ready + uses: actions/github-script@v7 + with: + script: | + github.rest.repos.createCommitComment({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.sha, + body: `✅ Deployed to staging: https://staging_${{ secrets.ARNS_NAME }}.arweave.net\n\nReady for production deployment.` + }) +``` + +### Production Deployment with Manual Approval + +```yaml title=".github/workflows/deploy-production.yml" +name: Deploy to Production + +on: + workflow_dispatch: # Manual trigger only + +jobs: + deploy-production: + runs-on: ubuntu-latest + environment: + name: production + url: https://${{ secrets.ARNS_NAME }}.arweave.net + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install & Build + run: | + npm ci + npm run build + + - name: Deploy to Production + env: + DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} + ARNS_NAME: ${{ secrets.ARNS_NAME }} + run: | + npx permaweb-deploy \ + --arns-name $ARNS_NAME \ + --wallet-file <(echo "$DEPLOY_KEY") \ + --ttl 3600 + + - name: Create Version Archive + env: + DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} + ARNS_NAME: ${{ secrets.ARNS_NAME }} + run: | + VERSION=$(date +%Y%m%d)-${{ github.run_number }} + npx permaweb-deploy \ + --arns-name $ARNS_NAME \ + --undername v$VERSION \ + --wallet-file <(echo "$DEPLOY_KEY") \ + --ttl 3600 \ + --skip-confirmation + echo "Archived at: https://v${VERSION}_${{ secrets.ARNS_NAME }}.arweave.net" + + - name: Notify Production Deployment + uses: actions/github-script@v7 + with: + script: | + github.rest.repos.createCommitComment({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.sha, + body: `🎉 Deployed to PRODUCTION: https://${{ secrets.ARNS_NAME }}.arweave.net` + }) +``` + + + Configure environments in GitHub Settings → Environments: + + - **development** - No protection rules (auto-deploy) + - **staging** - Optional: required reviewers + - **production** - Required reviewers (enforce manual approval) + + +--- + +## Managing Undernames via ArNS App + +For operations that don't involve new uploads (like rollbacks or updates), use the [ArNS app](https://arns.app) web interface. + +### Rollback to a Previous Version + + + + + +Navigate to your ArNS name in the ArNS app. You'll see all undernames and their transaction IDs. + +For example: +- `@` → `abc123...` (current production) +- `v1` → `def456...` (previous version) +- `staging` → `ghi789...` (staging version) + + + + + +Copy the transaction ID from the undername you want to rollback to (e.g., `v1`). + + + + + +Click on the `@` (base) record, paste the transaction ID from `v1`, and save. + +Production now serves the `v1` content - instant rollback! + + + + + + + Rollbacks on Arweave are instant because you're just updating a pointer to existing data. The old version is already on Arweave, permanent and accessible. + + +### Viewing All Undernames + +In the ArNS app, you can: + +- ✅ View all undernames and their transaction IDs +- ✅ See TTL values for each undername +- ✅ Update any undername's transaction ID +- ✅ Add new undernames manually +- ✅ Delete undernames you no longer need + +For detailed instructions, see: [Managing ArNS via UI](/build/guides/working-with-arns/manage-arns-ui) + +--- + +## Best Practices + +### Choosing a Versioning Strategy + +| Strategy | Best For | Pros | Cons | +|----------|----------|------|------| +| **Environment-Based** (dev, staging, prod) | Most applications | Simple, clear workflow, easy testing | No version history in URLs | +| **Version Numbers** (v1, v2, v3) | Long-term support needs | Clear version history, easy rollback | More undernames to manage | +| **Combined** (v2-staging, v2-prod) | Complex applications | Best of both worlds, comprehensive | Most complex to manage | +| **Component Architecture** (app, api, docs) | Large multi-part apps | Independent deployments, scalable | Coordination needed | + +### Naming Conventions + +**Recommended patterns:** + +```bash +# Environments +dev, staging, prod (rarely use prod since @ is production) + +# Version numbers +v1, v2, v3 + +# Version with minor +v2-1, v2-2 + +# Combined +v2-staging, v3-beta + +# Components +app, api, docs, admin, assets +``` + +**Avoid:** + +```bash +# Don't use periods (they become underscores anyway) +v1.5.2 → becomes v1_5_2 (confusing) + +# Don't use spaces +my app → becomes my_app (unclear) + +# Don't be overly verbose +very-long-descriptive-name (hard to type and remember) +``` + +### TTL (Time To Live) Guidelines + +Choose TTL based on update frequency: + +| Environment | Recommended TTL | Reasoning | +|-------------|----------------|-----------| +| Production (`@`) | 3600s (1 hour) | Stable content, good caching | +| Staging | 900s (15 min) | Test updates frequently | +| Development | 60s (1 min) | Rapid iteration | +| Version archives (v1, v2) | 3600s (1 hour) | Rarely change, maximize caching | +| Static assets | 86400s (24 hours) | Very stable content | + +**Set TTL with the `--ttl` flag:** + +```bash +permaweb-deploy --arns-name myapp --ttl 3600 # Production: 1 hour +permaweb-deploy --arns-name myapp --undername staging --ttl 900 # Staging: 15 min +permaweb-deploy --arns-name myapp --undername dev --ttl 60 # Dev: 1 min +``` + +### Testing Before Production + +**Recommended workflow:** + + + + + +```bash +npm run build +npm run typecheck +npm run lint +``` + + + + + +```bash +npm run deploy:dev +``` + +Test at: `https://dev_myapp.arweave.net` + + + + + +```bash +npm run deploy:staging +``` + +Test at: `https://staging_myapp.arweave.net` + + + + + +```bash +npm run test:integration -- --base-url=https://staging_myapp.arweave.net +``` + + + + + +- Functional testing (all features work) +- Cross-browser testing (Chrome, Firefox, Safari) +- Mobile testing (responsive design) +- Performance testing (load times) + + + + + +```bash +npm run deploy:prod +``` + +Live at: `https://myapp.arweave.net` + + + + + +```bash +npm run deploy:archive +``` + +Permanent archive at: `https://v2-1-0_myapp.arweave.net` + + + + + +### Security Considerations + + + **Critical**: Never commit wallet files or private keys to git. Always use environment variables or secure secret management. + + +**Best practices:** + +```bash +# .gitignore +wallet.json +*.jwk +.env +.env.local +``` + +**GitHub Secrets:** + +Store in Settings → Secrets and variables → Actions: + +- `DEPLOY_KEY` - Your wallet JSON or private key +- `ARNS_NAME` - Your ArNS name + +**Local development:** + +```bash +# .env (never commit) +DEPLOY_KEY_PATH=~/wallets/arweave-wallet.json +ARNS_NAME=myapp +``` + +--- + +## Common Workflows + +### Daily Development + +```bash +# Make changes to your code +git add . +git commit -m "Add new feature" +git push origin develop + +# GitHub Actions automatically deploys to dev +# https://dev_myapp.arweave.net +``` + +### Weekly Staging Release + +```bash +# Merge develop to main +git checkout main +git merge develop +git push origin main + +# GitHub Actions automatically deploys to staging +# https://staging_myapp.arweave.net + +# Test staging thoroughly +npm run test:integration -- --base-url=https://staging_myapp.arweave.net +``` + +### Production Release + +```bash +# Manually trigger production deployment in GitHub Actions +# (requires approval if environment protection is enabled) + +# Or deploy manually: +npm run deploy:prod + +# Archive this version +npm run deploy:archive +``` + +### Emergency Rollback + +If production has issues: + +1. Go to [ArNS app](https://arns.app) +2. Find your ArNS name +3. Copy transaction ID from previous version undername +4. Update `@` (base) record with that transaction ID +5. Save - instant rollback! + +You've learned how to use ArNS undernames for strategic versioning: + +**Key Concepts:** +- ✅ Undernames as sub-domains under your ArNS name +- ✅ Common patterns (environment, version, component) +- ✅ Automation with package.json and GitHub Actions +- ✅ Managing undernames via ArNS app +- ✅ Best practices for production workflows + +**Strategic Benefits:** +- **Cost-effective** - One ArNS name, unlimited versions +- **Organized** - Logical structure for environments and versions +- **Permanent** - All versions accessible forever +- **Simple** - Use permaweb-deploy for everything +- **Instant rollbacks** - Via ArNS app interface + +## Next Steps + + + } + > +
+

Try the web-based deployment alternative:

+
    +
  • No CLI required
  • +
  • Visual interface
  • +
  • GitHub integration
  • +
  • Perfect for quick testing
  • +
+
+
+ + } + > +
+

Learn to manage undernames in the ArNS app:

+
    +
  • View all undernames
  • +
  • Update transaction IDs
  • +
  • Rollback to previous versions
  • +
  • Adjust TTL values
  • +
+
+
+ + } + > +
+

For advanced use cases, use the SDK:

+
    +
  • Programmatic undername control
  • +
  • Custom automation scripts
  • +
  • Batch operations
  • +
  • Advanced workflows
  • +
+
+
+ + } + > +
+

Connect with other builders:

+
    +
  • Get help with deployments
  • +
  • Share your strategies
  • +
  • Stay updated on features
  • +
  • Contribute ideas
  • +
+
+
+
diff --git a/content/build/guides/index.mdx b/content/build/guides/index.mdx index 67d031ae0..6e9ab348f 100644 --- a/content/build/guides/index.mdx +++ b/content/build/guides/index.mdx @@ -18,11 +18,11 @@ Explore real-world applications and use cases for **Arweave** and **AR.IO** infr ## Getting Started - - **Build permanent websites** that can't be censored or taken down + + **Build permanent websites and dApps** that can't be censored or taken down **Key topics:** - - Arweave manifests for file routing + - Deploy blogs, Arweave/AO dApps, and EVM dApps - Permaweb deployment tools - ArNS domain integration @@ -38,7 +38,7 @@ Explore real-world applications and use cases for **Arweave** and **AR.IO** infr - + **Version and organize** your permanent website content **Key topics:** @@ -58,17 +58,6 @@ Explore real-world applications and use cases for **Arweave** and **AR.IO** infr - - **Deploy dApps easily** using the ArDrive web interface - - **Key topics:** - - ArDrive web deployment - - Manifest creation - - ArNS name assignment - - Version management - - - **Build a decentralized NFT minting app** with Arweave and Crossmint @@ -93,7 +82,7 @@ Explore real-world applications and use cases for **Arweave** and **AR.IO** infr ## Ready to Build? -**Start with websites** - Learn how to host decentralized websites with [Hosting Decentralized Websites](/build/guides/hosting-decentralized-websites). +**Start with websites** - Learn how to host unstoppable apps with [Hosting Unstoppable Apps](/build/guides/hosting-unstoppable-apps). **Want domains?** Explore [ArNS Primary Names](/build/guides/arns-primary-names) for decentralized domain management. diff --git a/content/build/guides/meta.json b/content/build/guides/meta.json index 92e238a2b..fa529ed35 100644 --- a/content/build/guides/meta.json +++ b/content/build/guides/meta.json @@ -4,8 +4,7 @@ "defaultOpen": false, "pages": [ "depin", - "hosting-decentralized-websites", - "deploy-dapp-with-ardrive-web", + "hosting-unstoppable-apps", "crossmint-nft-minting-app", "working-with-arns", "using-turbo-in-a-browser" diff --git a/content/build/guides/using-turbo-in-a-browser/html.mdx b/content/build/guides/using-turbo-in-a-browser/html.mdx index deb44e74c..dc6b7be6c 100644 --- a/content/build/guides/using-turbo-in-a-browser/html.mdx +++ b/content/build/guides/using-turbo-in-a-browser/html.mdx @@ -3,8 +3,6 @@ title: "Using Turbo SDK with Vanilla HTML" description: "Integrate Turbo SDK directly into vanilla HTML pages using CDN imports" --- -# Using Turbo SDK with Vanilla HTML - **Firefox Compatibility**: Some compatibility issues have been reported with the Turbo SDK in Firefox browsers. At this time the below framework examples diff --git a/content/build/guides/using-turbo-in-a-browser/index.mdx b/content/build/guides/using-turbo-in-a-browser/index.mdx index e8d7e77a8..8d54b33d1 100644 --- a/content/build/guides/using-turbo-in-a-browser/index.mdx +++ b/content/build/guides/using-turbo-in-a-browser/index.mdx @@ -3,8 +3,6 @@ title: "Using Turbo in a Browser" description: "Learn how to integrate Turbo SDK with different web frameworks and vanilla HTML" --- -# Using Turbo in a Browser - Integrate the **Turbo SDK** directly into your web applications for fast, reliable data uploads to Arweave. Choose the approach that best fits your development workflow and framework preferences. ## What You Can Build diff --git a/content/build/guides/using-turbo-in-a-browser/nextjs.mdx b/content/build/guides/using-turbo-in-a-browser/nextjs.mdx index 0b4dbacc4..9f2509295 100644 --- a/content/build/guides/using-turbo-in-a-browser/nextjs.mdx +++ b/content/build/guides/using-turbo-in-a-browser/nextjs.mdx @@ -3,8 +3,6 @@ title: "Using Turbo SDK with Next.js" description: "Configure Turbo SDK in a Next.js application with proper polyfills for client-side usage" --- -# Using Turbo SDK with Next.js - **Firefox Compatibility**: Some compatibility issues have been reported with the Turbo SDK in Firefox browsers. At this time the below framework examples diff --git a/content/build/guides/using-turbo-in-a-browser/vite.mdx b/content/build/guides/using-turbo-in-a-browser/vite.mdx index 436e6f354..7edbf0d7f 100644 --- a/content/build/guides/using-turbo-in-a-browser/vite.mdx +++ b/content/build/guides/using-turbo-in-a-browser/vite.mdx @@ -3,8 +3,6 @@ title: "Using Turbo SDK with Vite" description: "Configure Turbo SDK in a Vite application with proper polyfills for client-side usage" --- -# Using Turbo SDK with Vite - **Firefox Compatibility**: Some compatibility issues have been reported with the Turbo SDK in Firefox browsers. At this time the below framework examples diff --git a/content/build/guides/working-with-arns/arns-primary-names.mdx b/content/build/guides/working-with-arns/arns-primary-names.mdx index b53ddaf82..f4446f724 100644 --- a/content/build/guides/working-with-arns/arns-primary-names.mdx +++ b/content/build/guides/working-with-arns/arns-primary-names.mdx @@ -118,8 +118,8 @@ console.log(nameData.name); // e.g., "jonniesparkles" ## Ready to Learn More? - } > @@ -134,11 +134,11 @@ console.log(nameData.name); // e.g., "jonniesparkles" Use the ArIO SDK to programmatically manage ArNS records. - } > - Learn how to deploy and host permanent websites on Arweave. + Learn how to deploy and host permanent websites and dApps on Arweave. diff --git a/content/build/guides/working-with-arns/arns-undernames-versioning.mdx b/content/build/guides/working-with-arns/arns-undernames-versioning.mdx deleted file mode 100644 index 54e816377..000000000 --- a/content/build/guides/working-with-arns/arns-undernames-versioning.mdx +++ /dev/null @@ -1,259 +0,0 @@ ---- -title: "ArNS Undernames for Permasite Versioning" -description: "Use ArNS undernames to manage different versions and components of your permanent website" ---- - -Use **ArNS undernames** to organize and version your permanent website components. Undernames allow you to create sub-domains under your main ArNS name, making it easy to manage different versions, pages, and assets. - -## What Are Undernames? - -**Undernames** are sub-domains under your main ArNS name that can point to different Arweave transactions. They provide a structured way to organize your permanent website content. - -**Example structure:** - -- `yourname.arweave.dev` - Main site -- `v1_yourname.arweave.dev` - Version 1 -- `v2_yourname.arweave.dev` - Version 2 -- `api_yourname.arweave.dev` - API endpoints -- `docs_yourname.arweave.dev` - Documentation - -## Real-World Example: ArDrive - -The ArDrive website uses undernames to organize different components and versions: - -```json -{ - "@": { - "priority": 0, - "ttlSeconds": 3600, - "transactionId": "Vrf5_MrC1R-6rAk7o_E52DwOsKhyJmkSUqh0h5q4mDQ", - "index": 0 - }, - "dapp": { - "ttlSeconds": 3600, - "transactionId": "1ubf6cW8T5dYN3COApn8Yii4bA0HKoGeid-z2IjelTo", - "index": 1 - }, - "home": { - "ttlSeconds": 900, - "transactionId": "V9rQR06L1w9eLBHh2lY7o4uaDO6OqBI8j7TM_qjmNfE", - "index": 2 - }, - "v1_home": { - "ttlSeconds": 900, - "transactionId": "YzD_Pm5VAfYpMD3zQCgMUcKKuleGhEH7axlrnrDCKBo", - "index": 9 - }, - "v2_home": { - "ttlSeconds": 900, - "transactionId": "nOXJjj_vk0Dc1yCgdWD8kti_1iHruGzLQLNNBHVpN0Y", - "index": 10 - }, - "v3_home": { - "ttlSeconds": 900, - "transactionId": "YvGRDf0h2F7LCaGPvdH19m5lqbag5DGRnw607ZJ1oUg", - "index": 11 - } -} -``` - -**This structure provides:** - -- **`@`** - Main site (ardrive.arweave.dev) -- **`dapp`** - Application interface (dapp_ardrive.arweave.dev) -- **`home`** - Homepage (home_ardrive.arweave.dev) -- **`v1_home`** - Version 1 homepage (v1_home_ardrive.arweave.dev) -- **`v2_home`** - Version 2 homepage (v2_home_ardrive.arweave.dev) -- **`v3_home`** - Version 3 homepage (v3_home_ardrive.arweave.dev) - -## Use Cases for Undernames - -### 1. Website Versioning - -**Maintain multiple versions:** - -- `v1_yourname.arweave.dev` - Previous version -- `v2_yourname.arweave.dev` - Current version -- `beta_yourname.arweave.dev` - Beta testing -- `staging_yourname.arweave.dev` - Staging environment - -### 2. Component Organization - -**Separate different parts:** - -- `api_yourname.arweave.dev` - API endpoints -- `docs_yourname.arweave.dev` - Documentation -- `assets_yourname.arweave.dev` - Static assets -- `blog_yourname.arweave.dev` - Blog content - -### 3. Content Management - -**Organize by content type:** - -- `home_yourname.arweave.dev` - Homepage -- `about_yourname.arweave.dev` - About page -- `contact_yourname.arweave.dev` - Contact page -- `privacy_yourname.arweave.dev` - Privacy policy - -## Benefits of Undername Versioning - -**Easy access to versions:** - -- Users can access any version directly via URL -- No need to remember transaction IDs -- Clear versioning structure - -**Permanent version history:** - -- All versions remain accessible forever -- Historical record of your website evolution -- Easy rollback to previous versions - -**Organized content:** - -- Logical structure for different components -- Easy to manage and update -- Clear separation of concerns - -**Transferable with ANT:** - -- Undernames transfer with the main ArNS name -- Maintain ownership of all versions -- Sell or transfer entire website structure - -## How to Set Up Undernames - -**1. Register your main ArNS name:** - -- Choose your primary name (e.g., `myapp`) -- Register through [ArNS App](https://arns.app) - -**2. Create undernames:** - -- Use the ANT interface to add undernames -- Point each undername to different transaction IDs -- Set appropriate TTL values - -**3. Deploy different versions:** - -- Upload each version to Arweave -- Get transaction IDs for each version -- Update undername records - -## Example Implementation - -**Deploy version 1:** - -```bash -# Deploy to main site -npx permaweb-deploy --arns-name myapp --deploy-folder ./v1-build - -# Deploy to v1 undername -npx permaweb-deploy --arns-name myapp --undername v1 --deploy-folder ./v1-build -``` - -**Deploy version 2:** - -```bash -# Deploy to main site -npx permaweb-deploy --arns-name myapp --deploy-folder ./v2-build - -# Deploy to v2 undername -npx permaweb-deploy --arns-name myapp --undername v2 --deploy-folder ./v2-build -``` - -**Access different versions:** - -- `myapp.arweave.dev` - Current version -- `v1_myapp.arweave.dev` - Version 1 -- `v2_myapp.arweave.dev` - Version 2 - -## Direct Code Implementation - -For more control, you can manage undernames programmatically: - -```javascript -const { ARIO, ANT } = require("@ar.io/sdk"); -const { ArweaveSigner } = require("@ardrive/turbo-sdk"); -const fs = require("fs"); - -async function updateArNSRecord(arnsName, manifestId, undername) { - // Load your wallet JSON file - const jwk = JSON.parse(fs.readFileSync("./wallet.json", "utf8")); - - const ario = ARIO.mainnet(); - const pid = await ario.getArNSRecord({ name: arnsName }); - - const signer = new ArweaveSigner(jwk); - const ant = ANT.init({ processId: pid.processId, signer }); - - if (undername === "@") { - return await ant.setBaseNameRecord({ - transactionId: manifestId, - ttlSeconds: 900, - }); - } else { - return await ant.setUndernameRecord({ - undername: undername, - transactionId: manifestId, - ttlSeconds: 900, - }); - } -} -``` - -**Complete deployment example:** - -```javascript -const { TurboFactory } = require("@ardrive/turbo-sdk"); -const fs = require("fs"); - -async function deployWithUndername(arnsName, undername, distFolderPath) { - // Load your wallet JSON file - const jwk = JSON.parse(fs.readFileSync("./wallet.json", "utf8")); - - const turbo = TurboFactory.authenticated({ privateKey: jwk }); - - const { manifestResponse } = await turbo.uploadFolder({ - folderPath: distFolderPath, - dataItemOpts: { tags: [{ name: "App-Name", value: "ar.io docs deploy" }] }, - manifestOptions: { fallbackFile: "index.html" }, - }); - - await updateArNSRecord(arnsName, manifestResponse.id, undername); - console.log( - `Deployed to: https://${arnsName}.ar-io.dev${undername === "@" ? "" : `/${undername}`}` - ); -} - -// Usage -await deployWithUndername("myapp", "v1", "./build-v1"); -``` - -## Ready to Version Your Site? - - - } - > - Use the ArIO SDK to programmatically manage ArNS records. - - -} -> - Set up web3 identity with primary names. - - - } - > - Learn how to deploy and host permanent websites on Arweave. - - diff --git a/content/build/guides/working-with-arns/index.mdx b/content/build/guides/working-with-arns/index.mdx index 7399cd4ae..1e9b495e7 100644 --- a/content/build/guides/working-with-arns/index.mdx +++ b/content/build/guides/working-with-arns/index.mdx @@ -60,7 +60,7 @@ ArNS is a decentralized naming system that allows you to: icon={} title="Undername Versioning" description="Use undernames to manage different versions and components of your permanent website" - href="/build/guides/working-with-arns/arns-undernames-versioning" + href="/build/guides/hosting-unstoppable-apps/using-undernames-for-versioning" /> @@ -94,9 +94,9 @@ The smart contract system that manages ArNS name ownership, transfers, and recor - } > diff --git a/content/build/guides/working-with-arns/set-arns-records-programmatically.mdx b/content/build/guides/working-with-arns/set-arns-records-programmatically.mdx index 23bfb7a1f..04a4cc057 100644 --- a/content/build/guides/working-with-arns/set-arns-records-programmatically.mdx +++ b/content/build/guides/working-with-arns/set-arns-records-programmatically.mdx @@ -343,8 +343,8 @@ async function safeSetRecord(arnsName, undername, transactionId) { ## Ready to Get Started? - } > diff --git a/content/build/index.mdx b/content/build/index.mdx index 8a42cd557..6c6bf8bdf 100644 --- a/content/build/index.mdx +++ b/content/build/index.mdx @@ -72,9 +72,9 @@ If you're unfamiliar with Arweave's permanent storage and AR.IO Network we recom } - title="Hosting Decentralized Websites" + title="Hosting Unstoppable Apps" description="Build and deploy censorship-resistant web applications on the permanent web" - href="/build/guides/hosting-decentralized-websites" + href="/build/guides/hosting-unstoppable-apps" /> } @@ -86,7 +86,7 @@ If you're unfamiliar with Arweave's permanent storage and AR.IO Network we recom icon={} title="ArNS Undernames & Versioning" description="Manage subdomains and versioning for your ArNS names" - href="/build/guides/arns-undernames-versioning" + href="/build/guides/hosting-unstoppable-apps/using-undernames-for-versioning" /> } diff --git a/content/build/run-a-gateway/index.mdx b/content/build/run-a-gateway/index.mdx index 25eb55b6e..6e38c94e5 100644 --- a/content/build/run-a-gateway/index.mdx +++ b/content/build/run-a-gateway/index.mdx @@ -12,48 +12,21 @@ Join the decentralized network that powers permanent data access. Run your own * Choose the deployment approach that fits your needs - from local testing to production infrastructure. - Production Gateway Earn Rewards} - description={ - <> -

Join the AR.IO Network and earn ARIO tokens

-
-
Earn ARIO token rewards
-
Serve Wayfinder traffic
-
Cache and serve Arweave data
-
- - } + description="Join the AR.IO Network and earn ARIO tokens. Earn rewards by serving Wayfinder traffic and caching/serving Arweave data." href="/build/run-a-gateway/quick-start#production-setup-with-custom-domain" icon={} /> - -

Perfect for development and testing

-
-
Quick Docker setup
-
Test gateway features
-
No commitment required
-
- - } + description="Perfect for development and testing. Features quick Docker setup, ability to test gateway features, and no commitment required." href="/build/run-a-gateway/quick-start" icon={} /> - -

Optimize for specific use cases

-
-
• Data filtering options
-
• Performance tuning
-
• Advanced features
-
- - } + description="Optimize for specific use cases. Includes data filtering options, performance tuning, and advanced features." href="/build/run-a-gateway/manage/filters" icon={} /> diff --git a/content/build/upload/index.mdx b/content/build/upload/index.mdx index bf6b6b7d0..b27b3e091 100644 --- a/content/build/upload/index.mdx +++ b/content/build/upload/index.mdx @@ -13,6 +13,7 @@ import { Zap, Check, Image, + Rocket, } from "lucide-react"; Arweave enables **permanent data storage** with a single payment. Unlike traditional cloud storage that requires ongoing fees, your data is preserved forever. @@ -31,60 +32,19 @@ There are multiple ways to upload data to Arweave. Each has its own attributes a } - description={ - <> -

- Production-ready bundling service with enterprise features -

-
-
- Credit cards, AR, ETH, SOL, MATIC -
-
- Free uploads under 100 KiB -
-
- Automatic retry & confirmation -
-
- - } + description="Production-ready bundling service with enterprise features. Accepts credit cards, AR, ETH, SOL, MATIC. Free uploads under 100 KiB with automatic retry and confirmation." href="/build/upload/bundling-services" icon={Turbo} /> -

No-code solution for personal and business files

-
-
- Drag-and-drop interface -
-
- End-to-end encryption -
-
- Powered by Turbo -
-
- - } + description="No-code solution for personal and business files. Features drag-and-drop interface, end-to-end encryption, and powered by Turbo." href="https://app.ardrive.io" icon={ArDrive} /> -

Raw protocol access for advanced use cases

-
-
• AR tokens required
-
• Manual transaction handling
-
• Complex setup needed
-
- - } + description="Raw protocol access for advanced use cases. Requires AR tokens, manual transaction handling, and complex setup." href="https://arweave.org" icon={Arweave} /> @@ -182,4 +142,10 @@ Before uploading, learn best practices for structuring and tagging your data for description="Join our Discord community" href="https://discord.gg/cuCqBb5v" /> + } + />
diff --git a/content/learn/arns/index.mdx b/content/learn/arns/index.mdx index 619883bf3..0a685b1bd 100644 --- a/content/learn/arns/index.mdx +++ b/content/learn/arns/index.mdx @@ -4,7 +4,7 @@ description: "ArNS is a censorship-resistant naming system stored on Arweave, po --- import { Card, Cards } from "fumadocs-ui/components/card"; -import { Globe, Key, DollarSign, ShoppingCart } from "lucide-react"; +import { Globe, Key, DollarSign, ShoppingCart, Rocket } from "lucide-react"; ## What is ArNS? @@ -120,4 +120,10 @@ sequenceDiagram href="https://arns.app" icon={} /> + } + />
diff --git a/public/arlink-deploy-config.png b/public/arlink-deploy-config.png new file mode 100644 index 000000000..133f2faff Binary files /dev/null and b/public/arlink-deploy-config.png differ diff --git a/public/arlink-deployment-process.png b/public/arlink-deployment-process.png new file mode 100644 index 000000000..805643075 Binary files /dev/null and b/public/arlink-deployment-process.png differ diff --git a/public/arlink-history.png b/public/arlink-history.png new file mode 100644 index 000000000..5a9af4856 Binary files /dev/null and b/public/arlink-history.png differ diff --git a/public/arlink-homepage.png b/public/arlink-homepage.png new file mode 100644 index 000000000..b9c8ddfde Binary files /dev/null and b/public/arlink-homepage.png differ diff --git a/public/arlink-login.png b/public/arlink-login.png new file mode 100644 index 000000000..1dac5d657 Binary files /dev/null and b/public/arlink-login.png differ diff --git a/public/arlink-repo-select.png b/public/arlink-repo-select.png new file mode 100644 index 000000000..076ea332a Binary files /dev/null and b/public/arlink-repo-select.png differ diff --git a/redirects.mjs b/redirects.mjs index 3f4574962..28d5e5ae1 100644 --- a/redirects.mjs +++ b/redirects.mjs @@ -307,7 +307,7 @@ const redirects = [ }, { source: '/guides/ardrive-web', - destination: '/build/guides/deploy-dapp-with-ardrive-web', + destination: '/build/guides/hosting-unstoppable-apps/hosting-with-ardrive', permanent: true, }, { @@ -317,7 +317,7 @@ const redirects = [ }, { source: '/guides/permaweb-deploy', - destination: '/build/guides/hosting-decentralized-websites', + destination: '/build/guides/hosting-unstoppable-apps', permanent: true, }, { @@ -327,7 +327,7 @@ const redirects = [ }, { source: '/guides/managing-undernames', - destination: '/build/guides/arns-undernames-versioning', + destination: '/build/guides/hosting-unstoppable-apps/using-undernames-for-versioning', permanent: true, }, { @@ -380,6 +380,27 @@ const redirects = [ destination: '/build/guides', permanent: true, }, + // New hosting unstoppable apps redirects + { + source: '/build/guides/hosting-decentralized-websites', + destination: '/build/guides/hosting-unstoppable-apps', + permanent: true, + }, + { + source: '/build/guides/arns-undernames-versioning', + destination: '/build/guides/hosting-unstoppable-apps/using-undernames-for-versioning', + permanent: true, + }, + { + source: '/build/guides/working-with-arns/arns-undernames-versioning', + destination: '/build/guides/hosting-unstoppable-apps/using-undernames-for-versioning', + permanent: true, + }, + { + source: '/build/guides/deploy-dapp-with-ardrive-web', + destination: '/build/guides/hosting-unstoppable-apps/hosting-with-ardrive', + permanent: true, + }, // Contract pages { diff --git a/src/app/global.css b/src/app/global.css index 87e74977b..85242d6a4 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -17,3 +17,8 @@ Roboto, sans-serif; } + +/* Add spacing between main content and feedback section */ +.border-y.py-3 { + margin-top: 3rem; +} diff --git a/src/lib/source.ts b/src/lib/source.ts index 09fc5267e..e3cfb816c 100644 --- a/src/lib/source.ts +++ b/src/lib/source.ts @@ -21,6 +21,7 @@ export const source = loader({ // Handle custom SVG/PNG icons if (typeof icon === 'string' && (icon.endsWith('.svg') || icon.endsWith('.png'))) { return createElement(Image, { + key: icon, src: icon, alt: '', width: 16, @@ -30,6 +31,6 @@ export const source = loader({ } // Handle lucide-react icons - if (icon in icons) return createElement(icons[icon as keyof typeof icons]); + if (icon in icons) return createElement(icons[icon as keyof typeof icons], { key: icon }); }, });