Skip to content

JPROT
Docs Examples Showcase Publish GitHub
JPROTStart hereFirst siteWrite contentConfigureCustomizePublishCatalog elementsCLI referenceAPI referenceExamplesTroubleshootingFAQArchitectureComparisonShowcaseResumeBlogfrontmatter

On this page

Minimum settingsA full exampleAll optionsValidating your configPluginsSilence the linterLabels: every built-in text is overridableComplete key listCustom 404 pageSections: the portfolio builderUsing JSON instead of JSStartup hints (catches mistakes as you type)Drafts & production
JPROTStart hereFirst siteWrite contentConfigureCustomizePublishCatalog elementsCLI referenceAPI referenceExamplesTroubleshootingFAQArchitectureComparisonShowcaseResumeBlogfrontmatter

On this page

Minimum settingsA full exampleAll optionsValidating your configPluginsSilence the linterLabels: every built-in text is overridableComplete key listCustom 404 pageSections: the portfolio builderUsing JSON instead of JSStartup hints (catches mistakes as you type)Drafts & production
JPROT/Basic configuration

Basic configuration

All site-wide settings live in a single file at the project root: jprot.config.js (or jprot.config.json if you prefer JSON).

Minimum settings

export default {
  title: 'My site',                // name shown in the brand and browser tab
  tagline: 'Designer & developer', // subtitle under the homepage headline
  description: 'A short sentence for search engines and link previews.',
  url: 'https://example.com',      // set before publishing
  author: 'Your Name',
  email: 'you@example.com',
}

[!IMPORTANT] url drives the sitemap, RSS feed, and Open Graph link previews. It may be left empty during development; set your real address before publishing.

A full example

export default {
  // --- Site identity ---
  title: 'JPROT',
  tagline: 'A fully customizable site generator with no build step.',
  description: 'SEO description used in <meta name="description">.',
  lang: 'en',
  dir: 'ltr',
  url: 'https://jprot.dev',        // canonical base for sitemap/OG/RSS
  author: 'Jane Dev',
  email: 'hello@example.com',
  themeColor: '#4f46e5',

  // --- SEO / discovery enhancements ---
  ogImage: 'https://jprot.dev/img/og.png',  // explicit override; else auto SVG
  logo: '/img/logo.svg',                    // → Organization.logo + og:logo
  searchUrl: 'https://jprot.dev/search?q={search_term_string}',
  twitter: '@jprot',                        // → twitter:site
  ogLocale: 'en_US',                        // default: site.lang
  sameAs: ['https://github.com/jprot', 'https://x.com/jprot'],
  alternateLangs: [{ lang: 'ar', url: 'https://jprot.dev/ar/' }],

  // --- Homepage hero (used by the default Home component) ---
  hero: {
    title: 'JPROT',
    subtitle: 'Write Markdown. Customize everything.',
    avatar: '/img/avatar.png',
    links: [
      { label: 'Docs', url: '/getting-started' },
      { label: 'GitHub', url: 'https://github.com' },
    ],
  },

  // --- Sections ---
  projectsTitle: 'Selected Work',
  footerText: '© 2026 JPROT — Built with JPROT',

  // --- Content folders (defaults) ---
  projectsDir: 'projects',
  blogDir: 'blog',

  // --- Default layouts ---
  homeLayout: 'Home',
  defaultLayout: 'Page',

  // --- UI labels (all built-in text is overridable; for i18n) ---
  labels: {
    all: 'All',
    liveDemo: 'Live demo',
    source: 'Source',
    details: 'Details',
    noPosts: 'No posts yet.',
    searchPlaceholder: 'Search pages, posts, tags...',
    searchEmpty: 'No results',
    onThisPage: 'On this page',
    printResume: 'Download / Print',
    resumeExperience: 'Experience',
    resumeEducation: 'Education',
    resumeSkills: 'Skills',
    pageNotFound: 'Page not found',
    backToHome: 'Back to',
    home: 'Home',
    projects: 'Projects',
    blog: 'Blog',
  },

  // --- Homepage sections (composed from reusable components) ---
  sections: [
    {
      component: 'Stats',
      items: [
        { value: '15+', label: 'Projects shipped' },
      ],
    },
    {
      component: 'Skills',
      title: 'Skills',
      items: [
        { name: 'JavaScript', level: 90 },
        'Node.js', // plain strings default to level 80
      ],
    },
    {
      component: 'Experience',
      title: 'Experience',
      items: [
        { title: 'Senior Dev', company: 'Acme', period: '2022 — Now', description: 'What I did.' },
      ],
    },
    {
      component: 'Testimonials',
      title: 'Testimonials',
      items: [
        { text: 'Quote…', name: 'Sarah', role: 'Manager' },
      ],
    },
    {
      component: 'Contact',
      title: 'Contact',
      email: 'hello@example.com',
      social: [{ label: 'GitHub', url: 'https://github.com/you' }],
    },
    {
      component: 'Gallery',
      title: 'Gallery',
      items: [{ src: '/img/shot.png', alt: 'Screenshot' }],
    },
  ],

  // --- Extra tags injected into <head> ---
  head: `<link rel="icon" href="/favicon.ico">`,

  // --- Custom navigation (optional, overrides auto-generated nav) ---
  nav: [
    { label: 'Home', url: '/' },
    { label: 'About', url: '/about' },
  ],

  // --- Markdown parser options (all enabled by default) ---
  markdown: {
    inline: true,
    headings: true,
    lists: true,
    table: true,
    code: true,
    blockquote: true,
    hr: true,
    links: true,
    images: true,
    emphasis: true,
    footnotes: true,
    autolinks: true,
    taskLists: true,
  },
}

All options

OptionTypeDefaultDescription
titlestring—Site name (shown in the brand and <title>)
taglinestring—Short site tagline
descriptionstring—Meta description for SEO
langstring'en'<html lang> attribute
dirstring'ltr'Text direction ('ltr' or 'rtl')
urlstring—Canonical site URL (for sitemap, Open Graph, RSS, canonical tags)
basePathstring—Static export prefix for project sites, e.g. /REPOSITORY (folded into exported URLs and manifest.json)
docsbooleanfalseEnable docs navigation: breadcrumbs, sidebar reading order, previous/next links
catalogUrlstring—Base URL of the element catalog for jprot add / jprot search
authorstring—Author name for JSON-LD Person
avatarstring—Site-wide avatar image path (served from public/); hero.avatar overrides it on the homepage
emailstring—Contact email (mailto fallback in the Contact component)
themeColorstring#4f46e5PWA manifest + favicon + OG base color
ogImagestring—Explicit og:image URL (overrides the auto-generated SVG)
ogColorstring#4f46e5Background color of the auto-generated OG image
ogTextColorstring#ffffffText color of the auto-generated OG image
logostring—Logo path → JSON-LD Organization.logo + og:logo
searchUrlstring—Search endpoint → JSON-LD SearchAction (opt-in, e.g. https://example.com/search?q={search_term_string})
twitterstring—Twitter handle (@handle) → twitter:site
ogLocalestringlangog:locale value
sameAsstring[]—Social profile URLs → JSON-LD sameAs
alternateLangsarray—[{ lang, url }] → <link rel="alternate" hreflang> bridges
iconstring—Icon path for the PWA manifest
hero.titlestring—Homepage hero title
hero.subtitlestring—Homepage hero subtitle
hero.badgestring—Optional pill above the title (e.g. 'Available for new projects')
hero.avatarstring—Avatar image path (served from public/)
hero.linksarray—List of { label, url } hero buttons
projectsTitlestring'Projects'Homepage projects section heading
projectsDirstring'projects'Folder in content/ holding project posts
blogDirstring'blog'Folder in content/ holding blog posts
homeLayoutstring'Home'Component used for the homepage layout
defaultLayoutstring'Page'Component used for regular pages
labelsobjectbuilt-insOverride every built-in UI string (for i18n)
sectionsarray—Homepage sections; each { component, ...props } maps to a component
sidebarbooleantrueShow the docs sidebar on regular pages
showNavbooleantrueShow the nav links in the header (brand stays)
themePickerbooleantrueShow the ◈ theme-variant cycle button in the header
formspreestring—Formspree endpoint; enables the AJAX contact form
socialarray—Alternate social links read by the sample Footer
footerTextstring—Footer text (overrides the default year)
headstring—Raw HTML injected into <head>
navarrayautoOverrides the automatic navigation
markdownobjectall onToggle individual Markdown features
themesarraybuilt-insTheme variants offered by the picker/cycle button
lintobject—{ ignore: string[] } — globs of Markdown files jprot lint skips
pluginsstring[]—Plugin specifiers to run on every state build, e.g. ['./plugins/analytics.js'] (see Plugins)

Validating your config

jprot.d.ts gives you autocomplete and type errors in the editor. jprot check gives you the same validation in CI, with file and line:

jprot check          # exits 1 on any error
jprot check --strict # warnings count as errors too
✖ check: 1 error(s) in /path/jprot.config.js
  jprot.config.js:12  sections[0].component  no component named "Herro" — available: Hero, Skills

It validates the config against the schema and resolves the things a schema cannot know: that every sections[].component names a component that actually exists (including one added by a plugin), and that every entry in plugins: [...] resolves, imports, and completes setup().

Run it before jprot lint. check reads configuration; lint reads every content file and resolves every link against the real site. A broken config makes every other check meaningless.

Plugins

plugins lists files to run before each state build. Each one exports a setup(jprot) function:

export default {
  plugins: [
    './plugins/analytics.js',   // a file in this project
    'jprot-plugin-rss',         // an installed package
  ],
}

A plugin may add components, serve its own routes, extend Markdown, and subscribe to build/render hooks — and nothing else. A plugin that throws is reported and skipped without taking the site down, but a plugin that is half-applied is never possible: everything a plugin registers is committed only after setup() returns.

jprot check runs every plugin's setup() and fails if any of them throws, so a broken plugin is a red build rather than a blank page. Full guide in Plugins.

Silence the linter

jprot lint is site-aware — it reads your site through the same Content Graph the server uses, so every check is resolved against pages that actually exist rather than against a file listing. It checks frontmatter, internal links and #anchor targets, image alt text, unreachable pages, duplicate heading anchors, SEO metadata, navigation ordering, section components, and asset paths.

Two ways to opt a file out.

1. Glob patterns in the config — skip exact files or whole groups:

export default {
  lint: {
    ignore: [
      'old-notes.md',     // that exact file
      'projects/legacy/*.md', // every file in one folder
      'archive/**',       // a whole folder tree, recursively
    ],
  },
}

Patterns are matched against the Markdown path relative to the project root (content/ is omitted, and the .md suffix is optional). * never crosses a folder boundary; ** matches across folders.

2. Per-file frontmatter — opt a single page out from the file itself:

---
title: Redirected post
lint: false
---

Use these for content that is intentionally stale, machine-generated, or a mirror of an external post — everything else stays covered by the checks.

Labels: every built-in text is overridable

All visible UI strings come from site.labels. Leave a key out and the built-in value is used — this is how you translate the UI or match your own voice without touching a component:

labels: {
  all: 'Toutes',
  details: 'Voir plus',
  searchPlaceholder: 'Rechercher...',
  onThisPage: 'Sur cette page',
  resumeExperience: 'Expérience',
}

Complete key list

KeyDefaultWhere it appears
allAllProject/category filters
liveDemoLive demoProject card links
sourceSourceProject card links
detailsDetailsProject pages
noPostsNo posts yet.Empty blog listing
searchPlaceholderSearch pages, posts, tags...Search input
searchEmptyNo resultsEmpty search results
onThisPageOn this pageSidebar "On this page" heading
printResumeDownload / PrintResume layout button
resumeExperienceExperienceResume layout heading
resumeEducationEducationResume layout heading
resumeSkillsSkillsResume layout heading
pageNotFoundPage not found404 page title
backToHomeBack to404 page home link
homeHomeHome label
projectsProjectsProjects heading
blogBlogBlog heading

Any key you omit falls back to its built-in value; any extra key you add is ignored (unknown keys never break the server).

Custom 404 page

Drop a content/404.md file and JPROT serves it (with the full layout, header, sidebar, theme) any time a URL does not resolve — while still returning the correct 404 status:

---
title: Not found
restyle: true
---
# Oops, lost?

Try the [homepage](/).

Without that file, a plain branded 404 is generated (labels pageNotFound and backToHome).

Sections: the portfolio builder

site.sections is an array of section definitions. Each one names a component (built-in or your own override) and receives the rest of the object as props:

sections: [
  { component: 'Skills', title: 'Skills', items: [{ name: 'JS', level: 90 }] },
  { component: 'Contact', title: 'Contact', email: 'me@example.com' },
]

They render on the homepage after the hero, page content and projects (their order = array order). Remove them all to keep a minimal page.

Built-in portfolio components:

ComponentPropsRenders
Statsitems: [{ value, label }]Number strip
Skills`items: ['Node'] \[{ name, level }]`Animated skill bars
Experienceitems: [{ title, company, period, description }]Timeline list
Educationitems: [{ title, school, period, description }]Timeline list
Servicesitems: [{ icon, title, description, link }]Service cards
Awardsitems: [{ year, title, org, description }]Award list
Clientsitems: [{ name, url, logo }]Client/partner tiles
Testimonialsitems: [{ text, name, role }]Quote cards
Galleryitems: [{ src, alt }]Image grid
Contactemail, social: [{ label, url }]Contact bar
CTAtitle, text, label, urlCall-to-action banner

Any of these can be overridden by putting a component with the same name in theme/components/.

You can also add sections to a single page from frontmatter — list them in the page's YAML header (component props like items must then be written as YAML):

---
title: About
sections:
  - component: Skills
    items:
      - name: JS
        level: 90
---

Their order follows the list. To move the whole area elsewhere (or remove it from non-home pages), replace the Home/Page component — see Customization.

Using JSON instead of JS

JPROT also accepts a jprot.config.json:

{
  "title": "JPROT",
  "tagline": "A fully customizable site generator.",
  "lang": "en"
}

The JS version is recommended because it allows dynamic values and a head template string.

Startup hints (catches mistakes as you type)

When the server boots it double-checks two common mistakes and prints warnings in the terminal instead of failing silently:

  • A sections[].component that doesn't exist — with the list of components that are available:

    [jprot] section "NoSuchThing" (Hi) — no component named "NoSuchThing" found.
            Available: Awards, Blog, CTA, Clients, Contact, Education, Experience, ...
  • A config key that looks like a typo — with a did-you-mean suggestion:

    [jprot] config key "titll" not recognized — did you mean "title"?

Drafts & production

Set draft: true on any page while you're still writing it: it disappears from the navigation, sitemap, search index, RSS feeds and static exports, and returns 404 in production. The dev server keeps it reachable at its URL so you can preview it. jprot new post "Title" --draft creates a draft for you.

Next: Customize your site.

Previous← Write content NextCustomize →

© 2026 JPROT — Built with JPROT