Pro Tips

Building a website data extractor in React

Oct 14, 2025

Creating an intelligent web-powered form assistant is easier than ever. With the Fency SDK, you can extract structured insights from any given website and use them to suggest accurate values for your form fields automatically. Instead of manually copying and pasting information, Fency.ai analyzes the site’s content in real time and provides smart, context-aware recommendations that fit naturally into your form workflow.

This guide walks you through building a React + TypeScript project where the Fency SDK fetches and interprets web data, then integrates it with React Hook Form for dynamic suggestions — all beautifully styled using TailwindCSS.

Here’s an example of what your AI-powered form suggestions will look like:

1. Create a new Vite project

Start with a fresh React + TypeScript project:

npm create vite@latest website-data-extractor -- --template react-ts
cd website-data-extractor
npm install
npm

You should now see the default Vite app running at http://localhost:5173.

2. Add TailwindCSS

Install Tailwind and its Vite plugin:

npm

Import tailwind at the very top of your index.css:

@import "tailwindcss";

3. Configure Vite for Tailwind

Install @types/node to enable path and __dirname.

npm install -D @types/node

Update vite.config.ts so Tailwind and path aliases work:

import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

4. Install Zod and the and Hookform resolvers for schema validation

npm

5. Install React Hook Form for form handling

npm

6. Install Mantine as a component library

npm

Install PostCSS plugins and postcss-preset-mantine:

npm install --save-dev postcss postcss-preset-mantine postcss-simple-vars

Create a postcss.config.cjs file at the root of your application with the following content:

module.exports = {
  plugins: {
    'postcss-preset-mantine': {},
    'postcss-simple-vars': {
      variables: {
        'mantine-breakpoint-xs': '36em',
        'mantine-breakpoint-sm': '48em',
        'mantine-breakpoint-md': '62em',
        'mantine-breakpoint-lg': '75em',
        'mantine-breakpoint-xl': '88em'

Add styles imports and MantineProvider your application root component (usually main.tsx):

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import '@mantine/core/styles.css'
import { MantineProvider } from '@mantine/core'

createRoot(document.getElementById('root')!).render(
    <StrictMode>
        <MantineProvider>
            <App />
        </MantineProvider>
    </StrictMode>
)

7. Add Fency.ai

We’ll use Fency to utilize LLMs in React. If you haven’t already:

  1. Sign up at app.fency.ai/signup

  2. Create a new publishable key in the dashboard

Install the npm packages:

npm

Update main.tsx to include the FencyProvider and your newly created publishable key.

import { loadFency } from '@fencyai/js'
import { FencyProvider } from '@fencyai/react'
import { MantineProvider } from '@mantine/core'
import '@mantine/core/styles.css'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'

const fency = loadFency({ publishableKey: 'fency_pk_replace_with_your_own' })

createRoot(document.getElementById('root')!).render(
    <StrictMode>
        <MantineProvider>
            <FencyProvider fency={fency}>
                <App />
            </FencyProvider>
        </MantineProvider>
    </StrictMode>
)

8. Build the App

Update your App.tsx to include a simple app that extracts the content of a website and shows suggestions for the form fields.

import { useStructuredChatCompletions, useWebsites } from '@fencyai/react'
import { zodResolver } from '@hookform/resolvers/zod'
import { Alert, Button, Loader, TextInput } from '@mantine/core'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'

const linkSchema = z.object({
    link: z.string().min(1).url(),
})

const formSchema = z.object({
    companyName: z.string(),
    companyOrganizationNumber: z.string(),
    companyFullAddress: z.string(),
})

type ExampleState =
    | 'waiting_for_url'
    | 'getting_website_content'
    | 'getting_suggestions'
    | 'suggestions_received'
    | 'filling_form_error'

export default function App() {
    const [suggestions, setSuggestions] = useState<z.infer<
        typeof formSchema
    > | null>(null)
    const [formsState, setFormsState] =
        useState<ExampleState>('waiting_for_url')
    const { createWebsite } = useWebsites({
        async onTextContentReady(event) {
            setFormsState('getting_suggestions')
            console.log(event.textContent)
            const companyFormResponse =
                await chatCompletions.createStructuredChatCompletion({
                    responseFormat: formSchema,
                    gemini: {
                        messages: [
                            {
                                role: 'user',
                                content:
                                    'Find suggestions for the following form based on this content. Make sure to include all the relevant datapoints you can find ' +
                                    event.textContent,
                            },
                        ],
                        model: 'gemini-2.5-flash-lite-preview-06-17',
                    },
                })
            if (companyFormResponse.type === 'success') {
                setSuggestions(companyFormResponse.data.structuredResponse)
                setFormsState('suggestions_received')
            } else {
                setFormsState('filling_form_error')
            }
        },
    })
    const chatCompletions = useStructuredChatCompletions()
    const [formSubmitted, setFormSubmitted] = useState(false)

    const companyFormData = useForm({
        resolver: zodResolver(formSchema),
    })
    const linkForm = useForm({
        resolver: zodResolver(linkSchema),
        defaultValues: {
            link: 'https://www.allabolag.se/foretag/spotify-ab/stockholm/datacenters/5567037485',
        },
    })

    const submitForm = async (values: z.infer<typeof linkSchema>) => {
        setFormsState('getting_website_content')
        await createWebsite({
            url: values.link,
        })
    }

    const isLoading =
        formsState !== 'waiting_for_url' &&
        formsState !== 'suggestions_received'

    return (
        <div className="w-screen h-screen bg-gray-100 pt-10">
            <div className="flex flex-col gap-2 mb-2 max-w-2xl mx-auto bg-white p-4 rounded-lg">
                <form onSubmit={linkForm.handleSubmit(submitForm)}>
                    <TextInput
                        label="Link"
                        {...linkForm.register('link')}
                        error={linkForm.formState.errors.link?.message}
                    />
                    <div className="flex justify-end pt-2">
                        <Button
                            type="submit"
                            leftSection={
                                isLoading ? <Loader size="xs" /> : undefined
                            }
                            disabled={isLoading}
                        >
                            {getStateMeta(formsState).title}
                        </Button>
                    </div>
                </form>
                <form
                    onSubmit={companyFormData.handleSubmit(() => {
                        setFormSubmitted(true)
                    })}
                >
                    <TextInput
                        label="Company Name"
                        {...companyFormData.register('companyName')}
                        error={
                            companyFormData.formState.errors.companyName
                                ?.message
                        }
                    />
                    <Suggestions
                        suggestions={
                            suggestions?.companyName
                                ? [suggestions.companyName]
                                : []
                        }
                        onClick={(companyName) =>
                            companyFormData.setValue('companyName', companyName)
                        }
                    />
                    <TextInput
                        label="Company Organization Number"
                        {...companyFormData.register(
                            'companyOrganizationNumber'
                        )}
                        error={
                            companyFormData.formState.errors
                                .companyOrganizationNumber?.message
                        }
                    />
                    <Suggestions
                        suggestions={
                            suggestions?.companyOrganizationNumber
                                ? [suggestions.companyOrganizationNumber]
                                : []
                        }
                        onClick={(companyOrganizationNumber) =>
                            companyFormData.setValue(
                                'companyOrganizationNumber',
                                companyOrganizationNumber
                            )
                        }
                    />
                    <TextInput
                        label="Company Full Address"
                        {...companyFormData.register('companyFullAddress')}
                        error={
                            companyFormData.formState.errors.companyFullAddress
                                ?.message
                        }
                    />
                    <Suggestions
                        suggestions={
                            suggestions?.companyFullAddress
                                ? [suggestions.companyFullAddress]
                                : []
                        }
                        onClick={(companyFullAddress) =>
                            companyFormData.setValue(
                                'companyFullAddress',
                                companyFullAddress
                            )
                        }
                    />
                </form>
                {formSubmitted && (
                    <Alert
                        variant="light"
                        color="teal"
                        title="Form submitted successfully"
                    >
                        Form submitted successfully.
                    </Alert>
                )}
            </div>
        </div>
    )
}

function Suggestions({
    suggestions,
    onClick,
}: {
    suggestions: string[]
    onClick: (suggestion: string) => void
}) {
    return (
        <div className="flex gap-1 mt-2">
            {suggestions.map((suggestion) => (
                <Suggestion
                    key={suggestion}
                    value={suggestion}
                    onClick={() => onClick(suggestion)}
                />
            ))}
        </div>
    )
}

function Suggestion({
    value,
    onClick,
}: {
    value: string
    onClick: () => void
}) {
    return (
        <Button
            color="grape"
            size="xs"
            radius={'lg'}
            className="h-2"
            onClick={onClick}
        >
            {value}
        </Button>
    )
}

const getStateMeta = (
    state: ExampleState
): {
    title: string
} => {
    switch (state) {
        case 'waiting_for_url':
            return {
                title: 'Get suggestions',
            }
        case 'getting_website_content':
            return {
                title: 'Getting website content...',
            }
        case 'getting_suggestions':
            return {
                title: 'Getting suggestions...',
            }
        case 'suggestions_received':
            return {
                title: 'Suggestions received!',
            }
        case 'filling_form_error':
            return {
                title: 'Error filling form!',
            }
    }
}

9. Try It Out 🚀

  1. Run the app with npm run dev

  2. Click “Create Chat Completion”.

  3. Watch the LLM response stream in, beautifully rendered with markdown, code blocks, and formatting thanks to <Response>.

The complete codebase is available at https://github.com/fencyai/website-data-extractor-example