After building my personal portfolio website to showcase projects and experiments, I wanted to make it more interactive and useful.
The goal was to create something that could explain my work, using the same structured content that powers my site.
That idea became PETER, short for Personal Engineering Tech & Experience Responder.
It is an AI assistant built on AWS Bedrock that uses my portfolio content and project metadata as a knowledge base.
The system builds on the same pipeline that serves my website, with a few key additions for Bedrock and Knowledge Bases.
.mdx files into sanitized HTML and metadata.AWS Bedrock Knowledge Bases support metadata for each document. I use this to store values like title, date, stack, and canonical URL.
In the compiler script (mdxtohtml.mjs), each MDX file is parsed with gray-matter, flattened, and paired with rendered HTML.
const bedrock_metadata = {
"metadataAttributes": {
"url": getPageUrl(filePath),
...flattenObject(metadata)
}
}
This produces a small JSON file for each article:
{
"metadataAttributes": {
"url": "https://peterventon.co.uk/work/ai-powered-portfolio",
"title": "AI Powered Portfolio",
"stack_0": "AWS",
"projectType": "Personal project",
"date": "02/11/2025"
}
}
When a question triggers retrieval, Bedrock includes this metadata with the response.
The chat interface uses it to show clickable citations.
Since Bedrock does not support .mdx directly, I prebuild everything into clean HTML.
The compiler handles both rendering and metadata creation.
gray-matter.@mdx-js/mdx.react-dom/server.rehype./bedrock/html.const file = await unified()
.use(rehypeParse, { fragment: true })
.use(rehypeSanitize)
.use(rehypeMermaid)
.use(rehypeStringify)
.process(html)
await fs.writeFile(`${slug}.html`, file.toString())
await fs.writeFile(`${slug}.html.metadata.json`, JSON.stringify(bedrock_metadata))
The result is a clean HTML corpus with metadata that Bedrock can index and serve.
Each HTML file and its metadata are uploaded to an S3 bucket configured as a Knowledge Base source.
Instead of OpenSearch, this setup uses the new S3 Vector Indexing feature to embed and retrieve documents directly from S3.
When a user asks a question:
metadata.url and metadata.title as citations in the chat interface.Citations always link to an intentional route.
The compiler assigns each document a URL based on where it lives and its frontmatter status.
/work/[slug]/bedrock/unreleased?t={title}&slug={slug}/bedrock/missing/kb/[slug]This ensures every citation leads somewhere useful, even if the content is not public yet.
Example mapping:
function getPageUrl(filePath, metadata) {
const base = "https://www.peterventon.co.uk";
const slug = /* derive from filePath */;
const section = /* 'projects', 'kb-additional', etc. */;
if (section === "kb-additional") return `${base}/kb/${slug}`;
if (section === "projects") {
const isActive = metadata?.active !== false;
return isActive
? `${base}/work/${slug}`
: `${base}/bedrock/unreleased?t=${encodeURIComponent(metadata?.title ?? slug)}&slug=${slug}`;
}
return `${base}/bedrock/missing`;
}
My build process uses several environment variables such as API keys, prompts, and config values.
To keep these out of version control, I use AWS Systems Manager Parameter Store (SSM) to fetch them securely during builds.
The Amplify pipeline retrieves and exports each variable before running the build.
version: 1
frontend:
phases:
preBuild:
commands:
- npm ci
- npx playwright install chromium
- echo "Fetching secrets from SSM..."
- |
for secret in $(aws ssm get-parameters-by-path \
--path "<secret>" \
--with-decryption \
--region "eu-west-2" \
--query "Parameters[*].{Name:Name}" \
--output text); do
key=$(basename "$secret")
value=$(aws ssm get-parameter \
--name "$secret" \
--with-decryption \
--region "eu-west-2" \
--query "Parameter.Value" \
--output text)
if [ "$key" = "BEDROCK_SYSTEM_PROMPT" ]; then
echo "Writing System Prompt File..."
echo "$value" >> bedrock_system_prompt.txt
continue
fi
echo "$key=$value" >> .env
export "$key=$value"
done
build:
commands:
- npm run build
Sensitive values are stored encrypted at rest and fetched only at build time.
Nothing sensitive is ever committed to GitHub.
Originally, multiple chunks from the same document appeared as separate sources.
This cluttered the citation list, especially for long articles.

I fixed this by merging identical URLs and showing a count next to each title.

The result is cleaner and easier to read, while still showing how many times a document was used.
With the content and metadata pipeline in place, I began enriching the Knowledge Base with structured and external data.
stack, projectType, and date help retrieval relevance.This follows the same logic as BI modeling: a shared semantic layer that unifies multiple data sources.
/work/ai-powered-portfolio#metadata-generation) for more precise citations.PETER shows how data modeling principles like consistency and governance can improve even AI systems.
By combining my content, metadata, and Bedrock’s retrieval pipeline, the portfolio becomes a living knowledge system.
Every answer now has context, provenance, and a clear source, just like any well-modeled BI platform.
Model once. Reuse everywhere. Scale beyond dashboards.