惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
小众软件
小众软件
Vercel News
Vercel News
Last Week in AI
Last Week in AI
H
Help Net Security
The Cloudflare Blog
L
LangChain Blog
Microsoft Security Blog
Microsoft Security Blog
B
Blog RSS Feed
云风的 BLOG
云风的 BLOG
I
InfoQ
U
Unit 42
美团技术团队
人人都是产品经理
人人都是产品经理
雷峰网
雷峰网
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 叶小钗
Y
Y Combinator Blog
Hugging Face - Blog
Hugging Face - Blog
A
About on SuperTechFans
宝玉的分享
宝玉的分享
量子位
博客园_首页

Netlify Developers

AI model comparison using Netlify's AI Gateway | Netlify Developers Build an AI agent to automatically process and summarize form submissions | Netlify Developers Build an AI agent to automatically generate relevant images for blog posts | Netlify Developers Prerendering SPA content pages to enable AI agent access | Netlify Developers Identify image optimization opportunities with Netlify Observability | Netlify Developers Fix top 404 page not found errors with Netlify Observability | Netlify Developers Introducing AI Gateway: built-in AI model access on Netlify | Netlify Developers Introducing Observability: your project insights on Netlify | Netlify Developers From first publish to first update with AI agents | Netlify Developers How to use Agent Runners on Netlify | Netlify Developers Add forms to your project with AI + Netlify | Netlify Developers Track Real User Metrics | Netlify Developers Lighthouse performance scores on Netlify | Netlify Developers Review your AI project before you ship | Netlify Developers Using environment variables in AI Projects on Netlify | Netlify Developers Build prototypes and MVPs fast with AI + Netlify | Netlify Developers Pause auto-publishing on Netlify | Netlify Developers Run serverless functions, storage, and more natively in Nuxt dev | Netlify Developers Netlify Office Hours: The AX Arcade challenge | Netlify Developers Netlify Office Hours: Prompting with style | Netlify Developers Netlify Office Hours: RAG Apps with OpenAI + Netlify DB | Netlify Developers How to build a RAG application with Neon, Netlify & OpenAI | Netlify Developers Netlify Office Hours: Netlify DB (powered by Neon) | Netlify Developers Netlify Office Hours: Netlify MCP Server | Netlify Developers Netlify Office Hours: Netlify Vite Plugin | Netlify Developers Building MCPs with Netlify | Netlify Developers Deploying sites from your AI tool | Netlify Developers Adding your domain using Netlify API | Netlify Developers Build a context driven chat bot with Netlify Blob and OpenAI | Netlify Developers Simplify deployments with Netlify’s branch-matching environment variables | Netlify Developers
Add an image uploader to a file-based content site | Netl...
2024-05-14 · via Netlify Developers

Working with a git-based content workflow can be a powerful way to manage content for a site. But sometimes you need to add an image to a blog post or other content. Managing images in a git-based workflow either means adding another service or needing to be super diligent about managing images in your repository.

#TL;DR

This guide will show you how to add an image uploader to your Astro site to make it easy to manage images in your content. The uploader is only present in local development, and enforces a repeatable structure for storing images in your repository which surfaces when you serve your site in production.

Although this is specific to Astro, the concepts could be applied to other site framework where components are supported within your markdown content.

#JFDI (just deploy it)

If you’d prefer to just clone and deploy an example to explore, click the button below and you’ll very quickly have an example Astro site when an uploader ready to use in local development.

Deploy to Netlify

#Set up the project

If you already have a project ready to go, skip to the next section.

Let’s create a new Astro project.

Be sure to choose Use blog template when prompted on where to start. The blog template will have what we need to build out the image uploader.

> npm create astro@latest

Where should we create your new project? [default]

How would you like to start your new project? Use blog template

...

#Configure Astro

This approach uses an API route (server endpoint) to process the uploaded file. For this to work, we have to enable server-side rendering (SSR) in Astro. We can do this by leaving the site as static and enabling SSR only for specific routes.

#Add SSR to Astro

For API endpoints to work, we need to enable SSR in Astro. Many content sites can be built with Astro without server-side rendering, or with minimal server-side rendering. We’re going to assume that is the case for your site and use hybrid rendering mode.

Hybrid rendering mode defaults to rendering all pages as static. Pages and API endpoints that should be dynamically rendered on the server can be individually configured to do so.

import mdx from "@astrojs/mdx";

import sitemap from "@astrojs/sitemap";

import { defineConfig } from "astro/config";

export default defineConfig({

site: "https://example.com",

integrations: [mdx(), sitemap()],

output: "hybrid",

});

Using server mode

The rest of the guide assumes we’re working in hybrid mode. If you’re using server mode, you’ll have to adjust accordingly.

#Add uploader component

Add an uploader component to your Astro project. This will be the upload form that will post to an API endpoint to handle the file upload.

Place this file in your src/components directory.

---

interface Props {

id: string;

}

const { id } = Astro.props;

const url = Astro.url.href;

if (import.meta.env.MODE !== "development") {

throw new Error("This component can not be used in production");

}

---

<form action="/actions/upload" method="post" enctype="multipart/form-data">

<input type="hidden" value="{url}" name="url" required />

<input type="hidden" value="{id}" name="id" required />

<div>

<label for="{`file-input-${id}`}">Image file</label>

<input type="file" name="file" required id="{`file-input-${id}`}" accept="image/png, image/jpeg, image/gif" />

</div>

<div>

<label for="{`filename-input-${id}`}">Filename</label>

<input type="text" name="filename" id="{`filename-input-${id}`}" required />

</div>

<div>

<label for="{`alt-input-${id}`}">Alt text</label>

<input type="text" name="alt" id="{`alt-input-${id}`}" />

</div>

<input type="submit" value="Upload image" />

</form>

<script>

const form = document.querySelector("form")!;

form.addEventListener("submit", async (event) => {

event.preventDefault();

const formData = new FormData(form);

const response = await fetch(form.action, {

method: form.method,

body: formData,

});

if (!response.ok) {

console.error("Failed to upload image");

}

});

</script>

There are a few things to note about this component:

  • The component is designed to be used as a utility during development and does not function in production. After using it to upload an image, it replaces itself in the content with the appropriate markup. Unused upload components left in the code will fail a production build to prevent the form from being included in the final build and ensure you’ve uploaded all necessary images.
  • The form posts to /actions/upload, which we’ll set up in the next step.
  • We include both the url (from Astro) and id (prop) in the form to help the API endpoint know where to save the file and which content to replace.
  • The form is submitted asynchronously so that the browser will update the content when the image is added without refreshing the page.

#Add component to a blog post

As long as MDX is enabled, you can now add the component to any blog post on your site.

---

# ...

---

import Uploader from "../components/Uploader.astro";

Nisi nulla officia Lorem ad dolore consectetur cupidatat. Est veniam ut ipsum et. Aute ut excepteur occaecat reprehenderit pariatur in ea voluptate tempor commodo dolor laboris esse. Dolor pariatur dolore veniam sunt.

<Uploader id="1" />

Non ad eiusmod do exercitation esse voluptate amet exercitation do eiusmod duis ullamco velit. Irure ipsum qui labore laboris voluptate dolor nulla duis dolore pariatur duis id duis reprehenderit. Fugiat consequat elit veniam sit anim in do excepteur ipsum.

Check import path

Note that the path to the component may need to be adjusted based on your project structure.

View that post in your browser and you should now see the unstyle image upload form.

Inline image uploader form embedded in blog post

#Add the API endpoint

Now we need to set up the API endpoint to handle the file upload. This will be a simple function that saves the file to the repository, then replaces the uploader component with the image in the blog post.

Create a new file at src/actions/upload.ts.

import fs from "fs";

import type { APIRoute } from "astro";

export const prerender = false;

const FILE_TYPE_MAP: Record<string, string> = {

"image/jpeg": "jpg",

"image/png": "png",

"image/gif": "gif",

};

export const POST: APIRoute = async ({ request, url, redirect }) => {

if (import.meta.env.MODE !== "development") {

return new Response("Uploader not available", { status: 404 });

}

const formData = await request.formData();

const data = {

file: formData.get("file") as File,

url: formData.get("url") as string,

id: formData.get("id") as string,

alt: formData.get("alt") as string,

filename: formData.get("filename") as string,

};

if (!data.file) throw new Error("No file provided");

if (!data.url) throw new Error("No URL provided");

if (!data.id) throw new Error("No ID provided");

if (!data.filename) throw new Error("No filename provided");

const slug = new URL(data.url).pathname.split("/").filter(Boolean).pop();

// Identify the source file

const sourceFilePath = `./src/content/blog/${slug}.mdx`;

const sourceFile = fs.readFileSync(sourceFilePath, "utf-8");

const componentString = sourceFile.match(/<Uploader\s+id="(\d+)"\s*\/>/)?.[0];

if (!componentString) {

throw new Error("No Uploader component found in source file");

}

// Create folder for the post if it doesn't exist

const imageDirname = `./public/images/${slug}`;

if (!fs.existsSync(imageDirname)) {

fs.mkdirSync(imageDirname, { recursive: true });

}

// Save the file to the folder

const filename = `${data.filename}.${FILE_TYPE_MAP[data.file.type]}`;

const file = Buffer.from(await data.file.arrayBuffer());

const imageFilePath = `${imageDirname}/${filename}`;

fs.writeFileSync(imageFilePath, file);

// Replace the Uploader component with the image

const newComponentString = `![${data.alt || ""}](${imageFilePath.replace("./public", "")})`;

const newSourceFile = sourceFile.replace(componentString, newComponentString);

fs.writeFileSync(sourceFilePath, newSourceFile);

return new Response(JSON.stringify({ success: true }), {

headers: { "Content-Type": "application/json" },

});

};

Note that this creates a specific structure for storing your images:

  • Images are stored in a folder named for the blog post slug within the public/images directory.
  • The component is replaced in the source content file with the markdown image syntax. This is using a simple regex to find a specific pattern, <Uploader id="x" />, where x is the ID of the uploader component.

#Upload images!

That’s it! Now you can drop an image into the uploader form. The image will be saved to your repository in the proper directory, and then the uploader component will be replaced with markdown image syntax in your content file.

Where you had the component in your content file:

You’ll now have the markdown image syntax:

![alt-text](/images/<content-slug>/<image-filename>)

And the content will be automatically updated in your browser.

Image after being uploaded

#Next steps

I hope this gave you some inspiration for introducing tools that can augment your content workflow to make it more efficient, enjoyable, and maintainable.

Here are a few ideas for consideration as you bring this functionality into production.

#Add a platform adapter

You’ll also need an adapter when deploying the project. For deploying to Netlify, you can use Astro’s Netify adapter. To install and configure this automatically run the following command:

#Auto-import the component

Pro-tip! You can use astro-auto-import to avoid having to manually import the component into each blog post. I like doing this to keep my content files clean and focused on the content.

> yarn add astro-auto-import

Then add the integration to your astro.config.mjs file.

import mdx from "@astrojs/mdx";

import sitemap from "@astrojs/sitemap";

import { defineConfig } from "astro/config";

import AutoImport from "astro-auto-import";

export default defineConfig({

site: "https://example.com",

integrations: [

AutoImport({

imports: ["./src/components/Uploader.astro"],

}),

mdx(),

sitemap(),

],

output: "hybrid",

});