Yes, you can do this without any app. Since you want page-specific control, the cleanest way is to target individual pages by their handle inside your page template.
In your theme, open templates/page.liquid (or the page section if your theme uses JSON templates). Shopify gives every page a unique handle based on its URL, so you can add conditional schema like this:
{% if page.handle == 'about-us' %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "AboutPage",
"name": {{ page.title | json }},
"url": "{{ shop.url }}{{ page.url }}"
}
</script>
{% endif %}
{% if page.handle == 'contact' %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ContactPage",
"name": {{ page.title | json }},
"url": "{{ shop.url }}{{ page.url }}"
}
</script>
{% endif %}
Each page gets exactly the schema type it should have. Google maps real-world page types to specific schema, so an about page should use AboutPage, a contact page ContactPage, an FAQ page FAQPage, and so on.
If you have a lot of pages and don’t want a long if/else chain, the scalable version is to store the schema type and any custom fields as page metafields under Settings, Custom data, then read them dynamically:
{% assign schema_type = page.metafields.custom.schema_type %}
{% if schema_type %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "{{ schema_type }}",
"name": {{ page.title | json }},
"url": "{{ shop.url }}{{ page.url }}"
}
</script>
{% endif %}
That way you set the type per page from the admin without touching code again. Always wrap text values in the json filter like {{ page.title | json }} so any apostrophes or quotes don’t break the markup, then run the URL through Google’s Rich Results Test to confirm it parses.
Since you prefer coding it yourself, this keeps everything in your theme with no third party dependency.