From 60ecf914a567361436c251ebefbdd22aaa634859 Mon Sep 17 00:00:00 2001 From: wolovim <3621728+wolovim@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:34:13 -0400 Subject: [PATCH 01/10] feat: arweave lesson 5 init --- .../questions/arweave-101/5/quiz-1/Q1.json | 17 + .../questions/arweave-101/5/quiz-1/Q2.json | 16 + .../questions/arweave-101/5/quiz-1/Q3.json | 16 + .../questions/arweave-101/5/quiz-2/Q1.json | 16 + .../questions/arweave-101/5/quiz-2/Q2.json | 15 + .../questions/arweave-101/5/quiz-2/Q3.json | 16 + .../questions/arweave-101/5/quiz-2/Q4.json | 15 + .../questions/arweave-101/5/quiz-2/Q5.json | 15 + .../questions/arweave-101/5/quiz-2/Q6.json | 15 + .../questions/arweave-101/5/quiz-3/Q1.json | 15 + .../questions/arweave-101/5/quiz-3/Q2.json | 16 + .../questions/arweave-101/5/quiz-3/Q3.json | 15 + .../questions/arweave-101/5/quiz-4/Q1.json | 15 + .../questions/arweave-101/5/quiz-4/Q2.json | 16 + .../questions/arweave-101/5/quiz-4/Q3.json | 15 + .../questions/arweave-101/5/quiz-5/Q1.json | 15 + .../questions/arweave-101/5/quiz-5/Q2.json | 15 + .../questions/arweave-101/5/quiz-5/Q3.json | 15 + .../src/data/quizzes/arweave-101/5.json | 162 ++++ .../src/pages/tracks/arweave-101/5.mdx | 908 ++++++++++++++++++ 20 files changed, 1348 insertions(+) create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-1/Q1.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-1/Q2.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-1/Q3.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-2/Q1.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-2/Q2.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-2/Q3.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-2/Q4.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-2/Q5.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-2/Q6.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-3/Q1.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-3/Q2.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-3/Q3.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-4/Q1.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-4/Q2.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-4/Q3.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-5/Q1.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-5/Q2.json create mode 100644 apps/academy/src/data/questions/arweave-101/5/quiz-5/Q3.json create mode 100644 apps/academy/src/data/quizzes/arweave-101/5.json create mode 100644 apps/academy/src/pages/tracks/arweave-101/5.mdx diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-1/Q1.json b/apps/academy/src/data/questions/arweave-101/5/quiz-1/Q1.json new file mode 100644 index 00000000..32c8d5d7 --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-1/Q1.json @@ -0,0 +1,17 @@ +{ + "question": "What are the properties of SPAs?", + "options": [ + { + "answer": "You can use them without installation.", + "correct": true + }, + { + "answer": "They are platform independent.", + "correct": true + }, + { + "answer": "They execute and rener inside the browser.", + "correct": true + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-1/Q2.json b/apps/academy/src/data/questions/arweave-101/5/quiz-1/Q2.json new file mode 100644 index 00000000..722f4c22 --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-1/Q2.json @@ -0,0 +1,16 @@ +{ + "question": "Why are SPAs a good fit for Arweave?", + "options": [ + { + "answer": "As gateways only deliver static files, code must run on the client.", + "correct": true + }, + { + "answer": "Clint-side rendering scales better as server don’t have to execute any logic.", + "correct": true + }, + { + "answer": "They don’t require any JavaScript on the client." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-1/Q3.json b/apps/academy/src/data/questions/arweave-101/5/quiz-1/Q3.json new file mode 100644 index 00000000..488f023f --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-1/Q3.json @@ -0,0 +1,16 @@ +{ + "question": "What can SPAs do that static websites can’t?", + "options": [ + { + "answer": "Include data that isn’t available at build time.", + "correct": true + }, + { + "answer": "Render faster." + }, + { + "answer": "Enable complex user interactions.", + "correct": true + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q1.json b/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q1.json new file mode 100644 index 00000000..e580917d --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q1.json @@ -0,0 +1,16 @@ +{ + "question": "Where in this project do you write data to Arweave?", + "options": [ + { + "answer": "In Node.js, to deploy the DApp.", + "correct": true + }, + { + "answer": "In the browser, when creating and updating articles.", + "correct": true + }, + { + "answer": "On th server, when pre-rendering the articles." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q2.json b/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q2.json new file mode 100644 index 00000000..c2b6dac7 --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q2.json @@ -0,0 +1,15 @@ +{ + "question": "Why is the storage utility split into two files?", + "options": [ + { + "answer": "For efficiency, because the writing functionality requires many libraries and readers don’t need them.", + "correct": true + }, + { + "answer": "For security, because nobody should be able to write articles on your blog." + }, + { + "answer": "For stability, because some dependencies are very buggy and you don’t want the DApp to crash." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q3.json b/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q3.json new file mode 100644 index 00000000..7aca9c1e --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q3.json @@ -0,0 +1,16 @@ +{ + "question": "Why do you need custom tags?", + "options": [ + { + "answer": "To improve the discoverability of your uploaded files.", + "correct": true + }, + { + "answer": "To tell the gateways what type of file is in the transaction body.", + "correct": true + }, + { + "answer": "To enable compression of the uploaded files." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q4.json b/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q4.json new file mode 100644 index 00000000..ced06092 --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q4.json @@ -0,0 +1,15 @@ +{ + "question": "How does your blog update articles if data on Arweave is immutable?", + "options": [ + { + "answer": "It creates a transaction with a new article version and only displays the latest versions.", + "correct": true + }, + { + "answer": "It sends an UPDATE transaction to Arweave which circumvents the immutability." + }, + { + "answer": "It doesn’t, you can only create new articles." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q5.json b/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q5.json new file mode 100644 index 00000000..1744784b --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q5.json @@ -0,0 +1,15 @@ +{ + "question": "Can you filter GraphQL queries by the content of the transaction body?", + "options": [ + { + "answer": "No because transaction bodies can have an arbitrary size and format.", + "correct": true + }, + { + "answer": "Yes because gateways can parse transaction bodies." + }, + { + "answer": "Yes but only for transaction bodies that contain JSON." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q6.json b/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q6.json new file mode 100644 index 00000000..8fba5eb4 --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-2/Q6.json @@ -0,0 +1,15 @@ +{ + "question": "How do you retrieve the article content?", + "options": [ + { + "answer": "With the TXID and the gateway’s data endpoint.", + "correct": true + }, + { + "answer": "Via tags and a GraphQL query." + }, + { + "answer": "With Websockets and the article ID." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-3/Q1.json b/apps/academy/src/data/questions/arweave-101/5/quiz-3/Q1.json new file mode 100644 index 00000000..5ec6eff2 --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-3/Q1.json @@ -0,0 +1,15 @@ +{ + "question": "Why should you lazy-load some routes of your DApp?", + "options": [ + { + "answer": "Because not all functionality is used by all users.", + "correct": true + }, + { + "answer": "Because having all code in one file leads to bugs." + }, + { + "answer": "Because gateways can’t deliver files bigger than 100 KiB." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-3/Q2.json b/apps/academy/src/data/questions/arweave-101/5/quiz-3/Q2.json new file mode 100644 index 00000000..e8bc1e15 --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-3/Q2.json @@ -0,0 +1,16 @@ +{ + "question": "Why should you define your bundle chunks manually?", + "options": [ + { + "answer": "Chunk uploads under 100 KiB are free.", + "correct": true + }, + { + "answer": "It reduces storage costs by using chunks that are already on Arweave.", + "correct": true + }, + { + "answer": "Polyfill performance decreases with chunk size." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-3/Q3.json b/apps/academy/src/data/questions/arweave-101/5/quiz-3/Q3.json new file mode 100644 index 00000000..6aa10946 --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-3/Q3.json @@ -0,0 +1,15 @@ +{ + "question": "Why should you test your DApp with a regular web server before uploading it to Arweave?", + "options": [ + { + "answer": "Web servers behave more similarly to Arweave gateways than dev servers, which include hot reloading and other features.", + "correct": true + }, + { + "answer": "Web servers are faster in packaging your bundles for responses." + }, + { + "answer": "Dev servers aren’t open source." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-4/Q1.json b/apps/academy/src/data/questions/arweave-101/5/quiz-4/Q1.json new file mode 100644 index 00000000..6f1cd626 --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-4/Q1.json @@ -0,0 +1,15 @@ +{ + "question": "Why do you need Turbo Credits?", + "options": [ + { + "answer": "To upload files to Arweave that are bigger than 100 KiB.", + "correct": true + }, + { + "answer": "To improve gateway delivery performance." + }, + { + "answer": "To bribe Sam Williams." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-4/Q2.json b/apps/academy/src/data/questions/arweave-101/5/quiz-4/Q2.json new file mode 100644 index 00000000..7f44f60f --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-4/Q2.json @@ -0,0 +1,16 @@ +{ + "question": "How does the deployment script detect changed files?", + "options": [ + { + "answer": "The file names contain hashes that change when their content changes.", + "correct": true + }, + { + "answer": "The script can diff the current file names with the ones in the path manifest of the previous deployment.", + "correct": true + }, + { + "answer": "The script does integrity checks on the files." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-4/Q3.json b/apps/academy/src/data/questions/arweave-101/5/quiz-4/Q3.json new file mode 100644 index 00000000..c50b4986 --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-4/Q3.json @@ -0,0 +1,15 @@ +{ + "question": "Why do you have to set a base in the Vite configuration?", + "options": [ + { + "answer": "To ensure Vite generates relative URLs.", + "correct": true + }, + { + "answer": "To hide admin features from users." + }, + { + "answer": "To improve build performance." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-5/Q1.json b/apps/academy/src/data/questions/arweave-101/5/quiz-5/Q1.json new file mode 100644 index 00000000..f34409ca --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-5/Q1.json @@ -0,0 +1,15 @@ +{ + "question": "Why do you need the ANT process ID to update your ArNS name?", + "options": [ + { + "answer": "The ANT process controls the ArNS name.", + "correct": true + }, + { + "answer": "The ANT process handles your Turbo Credits." + }, + { + "answer": "Without the ANT process your change would be auctioned to the highest bidder." + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-5/Q2.json b/apps/academy/src/data/questions/arweave-101/5/quiz-5/Q2.json new file mode 100644 index 00000000..77760ef8 --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-5/Q2.json @@ -0,0 +1,15 @@ +{ + "question": "What’s the undername of the root record of an ArNS name?", + "options": [ + { + "answer": "@", + "correct": true + }, + { + "answer": "#" + }, + { + "answer": "ꙮ" + } + ] +} diff --git a/apps/academy/src/data/questions/arweave-101/5/quiz-5/Q3.json b/apps/academy/src/data/questions/arweave-101/5/quiz-5/Q3.json new file mode 100644 index 00000000..e5da3b4c --- /dev/null +++ b/apps/academy/src/data/questions/arweave-101/5/quiz-5/Q3.json @@ -0,0 +1,15 @@ +{ + "question": "Under which URL scheme is your DApp available after you point an ArNS name to its TXID?", + "options": [ + { + "answer": "https://./", + "correct": true + }, + { + "answer": "https:////" + }, + { + "answer": "https://./" + } + ] +} diff --git a/apps/academy/src/data/quizzes/arweave-101/5.json b/apps/academy/src/data/quizzes/arweave-101/5.json new file mode 100644 index 00000000..52812808 --- /dev/null +++ b/apps/academy/src/data/quizzes/arweave-101/5.json @@ -0,0 +1,162 @@ +{ + "title": "Final Quiz: Building a DApp on Arweave", + "questions": [ + { + "question": "Why would you build a DApp on Arweave as SPA?", + "options": [ + { + "answer": "Arweave gateways can't render HTML on the server side.", + "correct": true + }, + { + "answer": "Client-side rendering scales better as server don’t have to execute any logic.", + "correct": true + }, + { + "answer": "SPA's don’t require any JavaScript on the client." + } + ] + }, + { + "question": "What advantages do SPAs have over static websites?", + "options": [ + { + "answer": "They can use data that isn’t available at build time.", + "correct": true + }, + { + "answer": "They render faster." + }, + { + "answer": "They enable complex user interactions.", + "correct": true + } + ] + }, + { + "question": "Why did you split the storage utility in two files?", + "options": [ + { + "answer": "For efficiency, because the writing functionality requires many libraries and readers don’t need them.", + "correct": true + }, + { + "answer": "For security, because nobody should be able to write articles on your blog." + }, + { + "answer": "For stability, because some dependencies are very buggy and you don’t want the DApp to crash." + } + ] + }, + + { + "question": "Why DON'T you need custom tags?", + "options": [ + { + "answer": "To improve the discoverability of your uploaded files." + }, + { + "answer": "To tell the gateways what type of file is in the transaction body." + }, + { + "answer": "To enable compression of the uploaded files.", + "correct": true + } + ] + }, + + { + "question": "How does your blog mutate files on Arweave?", + "options": [ + { + "answer": "It doesn't, instead it creates a transaction with a new article version.", + "correct": true + }, + { + "answer": "It sends an UPDATE transaction to Arweave which circumvents the immutability." + }, + { + "answer": "It doesn’t, you can only create new articles." + } + ] + }, + { + "question": "How do you filter GraphQL queries by the content of the transaction body?", + "options": [ + { + "answer": "You can't; transaction bodies can have an arbitrary size and format.", + "correct": true + }, + { + "answer": "By using the data attribute." + }, + { + "answer": "By uploading a JSON file, other formats aren't supported." + } + ] + }, + { + "question": "Is the automatic chunk creation of Vite well suited for Arweave deployments?", + "options": [ + { + "answer": "No, it can lead to big chunks that change with only small updates.", + "correct": true + }, + { + "answer": "Yes, by default Vite generates chunks that are under 100 KiB." + }, + { + "answer": "No, Vite applies ZIP compression, which breaks the gateway cache." + } + ] + }, + { + "question": "Can your deployment script filter out previously deployed files?", + "options": [ + { + "answer": "Yes, the script checks the file names, which contain hashes that change when their content changes.", + "correct": true + }, + { + "answer": "Yes, the script can compare the current file names with the ones in the path manifest of the previous deployment.", + "correct": true + }, + { + "answer": "No, the script would need to compare files on Arweave with the local ones, which is impossible." + } + ] + }, + { + "question": "Is the default Vite configuration sufficient for Arweave deployments?", + "options": [ + { + "answer": "No, Vite generates absolute URLs by default, which don't work with TXIDs in the path.", + "correct": true + }, + { + "answer": "No, Vite doesn't add hashes to the index.html, which makes change detection hard.", + "correct": true + }, + { + "answer": "Yes, it builds the optimal bundles for gateways." + } + ] + }, + { + "question": "What is NOT the undername of the root record of an ArNS name?", + "options": [ + { + "answer": "@" + }, + { + "answer": "$", + "correct": true + }, + { + "answer": "ROOT", + "correct": true + } + ] + } + ] +} diff --git a/apps/academy/src/pages/tracks/arweave-101/5.mdx b/apps/academy/src/pages/tracks/arweave-101/5.mdx new file mode 100644 index 00000000..5e836120 --- /dev/null +++ b/apps/academy/src/pages/tracks/arweave-101/5.mdx @@ -0,0 +1,908 @@ +--- +title: Arweave 101: Lesson 4 +description: Deploy a DApp to Arweave. +icons: ["arweave", "fundamentals"] +--- + +import LessonLayout from "../../../components/LessonLayout"; +import Callout from "../../../components/mdx/Callout"; +import QuizStatusChecker from "../../../components/mdx/QuizStatusChecker"; +import Question from "../../../components/mdx/Question"; +import LessonQuestionsModal from "../../../components/mdx/LessonQuestionsModal"; +import LessonInformationalModal from "../../../components/mdx/LessonInformationalModal"; + + + +## About this lesson + +Greetings! I welcome you to the second project lesson of the Arweave 101 track. In the previous lesson, you learned how to build and deploy a static website to Arweave. The website only allowed users to view the content but not edit it. In this lesson, you will go to the next step: build a blog to create and update articles inside the browser. + +You will create a single-page application (SPA) with [Vite](https://vitejs.dev/), a popular web application bundler. You will use React as a UI framework and handle Arweave uploads with the Turbo SDK in the browser. + +## Prerequisites + +You must **complete lessons 1, 2, and 3**. You need the **wallet address you charged with Turbo Credits** in lesson 2 and **the ArNS name you registered** in lesson 3. + +You need a basic understanding of web technologies like HTTPS, HTML, JavaScript, Node.js, and React. + +A **basic understanding of blockchains** is helpful, too. At least you should understand what wallets and transactions are. + +A **browser, Node.js, and an ArConnect wallet** to try the examples. + +Since using web crypto functions requires HTTPS, you might want to use a cloud IDE like GitHub Codespaces or a service like lcl.host. + +## Why a Single-Page Application? + +While server-side rendering is all the hype, the fastest way to get a DApp up and running on Arweave is to follow the SPA approach. Arweave gateways can’t execute your code, so static generation and client-side rendering are the way to go. In the previous lesson, you rendered HTML at build-time; now, you’ll render it dynamically in the browser. + +An SPA can be slower than a static website but more flexible. It enables you to include data stored on Arweave into the rendering that wasn’t available at build time, so you don’t have to build and deploy a new release every time something changes. + + + + + + + +## Creating the DApp + +You will build a blog with React and use Vite as the JavaScript bundler. In contrast to the prior version of the blog, this one will allow you to create and update articles in the browser. + +Figure 1 illustrates the architecture of the new blog. React and React Router form the base for the pages, which will use a storage utility that interacts with Arweave through the ArweaveKit and Turbo SDK. Vite will build the app, and the Turbo SDK will deploy it on Arweave. This means there are two locations where you write data to Arweave: + +1. In a Node.js deployment script. +2. In the DApp in the browser. + +TODO +_Figure 1: Permablog V2 architecture_ + +### Creating the Project and Installing the Dependencies + +First, you create a new Vite project based on the React template: + +```bash +npm create vite@latest permablog-v2 -- --template react +``` + +Next, you install the dependencies. + +- React Router for navigation. +- The Turbo SDK for uploading new articles to Arweave. +- The AR.IO SDK is used to change your ArNS with a script. +- Arweave Wallet Kit gives you a nice button to log in to create and update articles. +- Arweave Kit will query articles from Arweave gateways via GraphQL. + +```bash +cd permablog-v2 +npm i react-router-dom @ardrive/turbo-sdk @ar.io/sdk arweave-wallet-kit arweavekit +``` + +After you install the dependencies, you have everything ready to build and deploy your new blog. + +### Creating a Storage Utility + +The first task is implementing a storage utility. It will handle all interactions with Arweave and give you a simple interface for creating, reading, and updating articles in the rest of your code. + +You will implement the read and write functionality in separate files. This way, the pages that only need to read from the storage don’t have to fetch all the writing functions and dependencies. + +#### Creating the Writing Functionality + +The first part of the storage is the writing functionality, which allows you to upload new and updated articles to Arweave. + +Start by creating a new file at `src/utilities/storage.write.js`, then add the following snippets to complete the file. + +#### Including the Modules and Creating Constants + +You start with the dependencies—the Turbo SDK for uploading articles. + +```javascript +import * as TurboSdk from "@ardrive/turbo-sdk/web"; +``` + +Next, you add the constants, which define some configurations: the name of your blog. You need it later to query the articles you created for this blog. + +```javascript +export const APPLICATION_NAME = "Permablog V2"; +``` + +#### Implementing the Functions + +The `uploadArticle` function converts an article object into a format suitable for the Turbo SDK. Then, it creates an instance of the SDK, connects with the ArConnect wallet of the current browser, and uploads the converted article as a file. + +The `uploadFile` function accepts tags that enable you to customize the transaction used to upload your article. You can use these tags in GraphQL queries to find your uploads on Arweave. It returns the TXID for the new version of the article. + +Most of the tags are just general information that makes it easier for others to categorize your data, but a few are important, and you will use them later: + +- The `Content-Type` tag ensures gateways will deliver your articles with the correct mime type to the browser. +- The `Application` tag allows you to filter for articles from that blog if you have multiple ones. +- The `Article-Id` tag ensures different versions of the same article have a correlating ID, so you can filter out older versions when listing all articles. +- The `Created-At` tag is a nice example of how you can include some parts of your content into tags to make it available via GraphQL. It does not need to be a query, but it’s helpful to display the creation dates of articles when listing them. +- `Topic` tags are generated from an article's topic list. Think of them as Arweave's hashtags. + + + Note: Gateways come with GraphQL endpoints, but they can’t look into the body of a transaction; + they can only access general transaction data and tags. Making tags the only way to customize your + uploads for queries. + + +```javascript +const uploadArticle = async (article) => { + const articleBlob = new Blob([JSON.stringify(article)]); + const turbo = await TurboSdk.TurboFactory.authenticated({ + signer: new TurboSdk.ArconnectSigner(window.arweaveWallet), + }); + const result = await turbo.uploadFile({ + fileStreamFactory: () => articleBlob.stream(), + fileSizeFactory: () => articleBlob.size, + dataItemOpts: { + tags: [ + { name: "Content-Type", value: "application/json" }, + { name: "Application", value: APPLICATION_NAME }, + { name: "Article-Id", value: article.id }, + { name: "Type", value: "blog-post" }, + { name: "Title", value: article.title }, + { name: "Created-At", value: article.createdAt.toISOString() }, + ...article.topics.map((topic) => ({ name: "Topic", value: topic })), + ], + }, + }); + return result.id; +}; +``` + +The `updateArticle` function comes next. It’s relatively straightforward, as the `uploadArticle` function above does most of the work. It uploads a new article version to Arweave. The TXID will change, but the article's ID will stay the same. + +```javascript +export const updateArticle = async (article) => ({ + ...article, + txId: await uploadArticle(article), +}); +``` + +The `createArticle` function uses the `updateArticle` function. However, it also adds an ID to the article, so it doesn’t correlate with any other article. This ID will stay the same on every update. + +```javascript +export const createArticle = async (article) => + await updateArticle({ + ...article, + id: crypto.randomUUID(), + }); +``` + +Now that you can upload your articles, let’s ensure you can also download them. + +### Creating the Reading Functionality + +The reading part of the storage will include a GraphQL query to list all articles and the gateway's data endpoint to retrieve the content of each article. + +Start by creating a new file at `src/utilities/storage.read.js`, then add the following snippets to complete the file. + +#### Including the Modules and Creating Constants + +You start with the dependencies—the Turbo SDK for uploads and the Arweave Kit for GraphQL queries. + +```javascript +import * as ArweaveKit from "arweavekit/graphql"; +``` + +Next, you add the constants, which define some configurations: the name of your blog and your Arweave wallet address. You need them later to query for articles you created for this blog. + +```javascript +export const APPLICATION_NAME = "Permablog V2"; +export const BLOG_OWNER_ADDRESS = "xyz...123"; +``` + +#### Implementing the Functions + +You start with the GraphQL query. It will get all transactions signed by your wallet address and have an `Application` tag with your blog’s name. It returns the newest 100 matching transactions and sorts them from new to old. This way, the newest article versions will be on top. It will return the ID of each TX, which you need to fetch the article content and the tags, as you will display article creation dates when listing them. + +```javascript +const LOAD_ARTICLES_QUERY = ` query($owner: String!, $application: String!) { + transactions( + sort: HEIGHT_DESC + first: 100 + owners: [$owner] + tags: [{ name: "Application", values: [$application] }] + ) { + edges { + node { + id + tags { name value } + } + } + } + }`; +``` + +The `getArticles` function uses the GraphQL query to fetch all article versions and filters only the newest version of each article. The older ones are always available, as they’re permanently stored on Arweave, but your users will only see the latest versions. + +The GraphQL response won’t include the article content, as it’s stored inside the transaction body, which isn’t accessible in a GraphQL query because it can have an arbitrary size and data format. You can only query the transaction data, but properties like title and creation date are enough to create a list of articles. + +```javascript +export const getArticles = async () => { + const result = await ArweaveKit.queryGQL(LOAD_ARTICLES_QUERY, { + filters: { + owner: BLOG_OWNER_ADDRESS, + application: APPLICATION_NAME, + }, + gateway: "arweave.developerdao.com", + }); + + if (!result.data || !result.data.transactions.edges) return []; + + const txs = result.data.transactions.edges.map((e) => e.node).reverse(); + const latestArticleVersions = {}; + + for (const tx of txs) { + const articleId = tx.tags.find((tag) => tag.name === "Article-Id")?.value; + + if (!articleId || latestArticleVersions[articleId]) continue; + + const title = tx.tags.find((tag) => tag.name === "Title")?.value || ""; + const createdAt = new Date(tx.tags.find((tag) => tag.name === "Created-At")?.value || 0); + const topics = tx.tags.filter((tag) => tag.name === "Topic").map((tag) => tag.value); + + latestArticleVersions[articleId] = { + id: articleId, + txId: tx.id, + title, + createdAt, + topics, + }; + } + + return Object.values(latestArticleVersions); +}; +``` + +The last function is `getArticle`, which loads only one article with content so that users can read more than just the title. You stored the article objects as JSON inside transaction bodies, which means you need to revive the date objects inside createdAt and add the TXID from the current version of the article. + +```javascript +export const getArticle = async (txId) => { + const response = await fetch(`https://arweave.developerdao.com/${txId}`); + const articleJson = await response.text(); + const partialArticle = JSON.parse(articleJson, (key, value) => + key === "createdAt" ? new Date(value) : value, + ); + return { ...partialArticle, txId }; +}; +``` + +After finishing the storage utility, you can use it on your pages to interact with Arweave in the browser. So, let’s move on to the pages! + + + + + + + + + + +## Creating the Pages + +Now, let’s create the pages. Your blog will have three of them: + +1. A home page that displays a list of articles. +2. An article page to read an article. +3. An editor page that allows you to create new articles and modify existing ones. + +### Creating the Home Page + +The home page will display a list of articles and a “Connect Wallet” button that you’ll use later to log in and access editor functionality. + +It exports two functions: + +1. React Router calls the `loader` function before rendering your page to get the data for the page. For this page, it will fetch all the articles. +2. The `Component` function is a React component that will render the actual HTML for the page. + + + Note: The names of these functions are a convention, so React Router can find them when importing + the files later. + + +Create a new file at `src/pages/home.jsx` to create the home page and add the following code: + +```javascript +import * as ReactRouter from "react-router-dom"; +import * as WalletKit from "arweave-wallet-kit"; +import * as StorageRead from "../utilities/storage.read"; + +export const loader = async () => await StorageRead.getArticles(); + +export function Component() { + const connection = WalletKit.useConnection(); + const articles = ReactRouter.useLoaderData(); + + return ( + <> +

Permablog

+ {connection.connected && Create Article} + +

Articles

+
    + {articles.length === 0 &&
  • No articles found.
  • } + {articles.map((article) => ( +
  • + {article.title}( + {article.createdAt.toLocaleDateString()}) +
  • + ))} +
+ + ); +} +``` + +### Creating the Article Page + +The article page lets users read the article content. It will use the TXID from the URL to determine the current article. + +Create a new file at `src/pages/article.jsx` and add this code: + +```javascript +import * as ReactRouter from "react-router-dom"; +import * as WalletKit from "arweave-wallet-kit"; +import * as StorageRead from "../utilities/storage.read"; + +export const loader = async ({ params }) => await StorageRead.getArticle(params["articleId"]); + +export function Component() { + const { connected } = WalletKit.useConnection(); + const article = ReactRouter.useLoaderData(); + return ( + <> +

+ Permablog +

+ {connected && ( + Edit Article + )} + +
+

{article.title}

+ {article.topics.map((t) => "#" + t).join(" ")} +

Created at {article.createdAt.toLocaleDateString()}

+

{article.content}

+ + ); +} +``` + +### Creating the Editor Page + +The editor is the heart of this blog, making it an interactive DApp. Its `loader` function will either load an existing article from Arweave or create a new one. The `Component` function will always create a new article, based on the one supplied by the loader function, but call different storage functions depending on the current article. You use the `createArticle` function if it’s a new article and the `updateArticle` function if it’s an existing article. The former will override the ID of the article. + +Create a new file at `src/pages/editor.jsx` and add this code: + +```javascript +import * as ReactRouter from "react-router-dom"; +import * as StorageRead from "../utilities/storage.read"; +import * as StorageWrite from "../utilities/storage.write"; + +export const loader = async ({ params }) => { + const articleId = params["articleId"]; + if (articleId === "new") return { id: "new" }; + if (articleId) return StorageRead.getArticle(articleId); +}; + +export function Component() { + const navigate = ReactRouter.useNavigate(); + const article = ReactRouter.useLoaderData(); + const isNewArticle = article.id === "new"; + + const handleArticleUpdate = async (event) => { + event.preventDefault(); + const data = new FormData(event.currentTarget); + + const newArticle = { + id: article.id, + createdAt: new Date(), + title: data.get("title"), + content: data.get("content"), + topics: data + .get("topics") + .split(",") + .map((t) => t.trim()), + }; + + if (!newArticle.title || !newArticle.topics || !newArticle.content) return; + + const { txId } = isNewArticle + ? await StorageWrite.createArticle(newArticle) + : await StorageWrite.updateArticle(newArticle); + + navigate("/" + txId); + }; + + return ( + <> +

+ Permablog +

+

{isNewArticle ? "Create New Article" : "Update Article"}

+
+ +
+ +
+