diff --git a/common/eda_text.cpp b/common/eda_text.cpp
index 50e7024246..f08c56d07c 100644
--- a/common/eda_text.cpp
+++ b/common/eda_text.cpp
@@ -983,7 +983,33 @@ bool EDA_TEXT::ValidateHyperlink( const wxString& aURL )
 {
     wxURL url;
 
-    return aURL.IsEmpty() || url.SetURL( aURL ) == wxURL_NOERR;
+    return aURL.IsEmpty() || url.SetURL( aURL ) == wxURL_NOERR || IsGotoPageHyperlink( aURL );
+}
+
+
+bool EDA_TEXT::IsGotoPageHyperlink( const wxString& aURL, int* aDestination )
+{
+    wxString dest;
+
+    if( !aURL.StartsWith( "goto:", &dest ) )
+        return false;
+
+    long num;
+    bool retval = dest.ToLong( &num );
+
+    if( !retval || num < 0 )
+        return false;
+
+    if( aDestination )
+        *aDestination = static_cast<int>( num );
+
+    return true;
+}
+
+
+wxString EDA_TEXT::GotoPageHyperlinkString( const int& aDestination )
+{
+    return wxString::Format( "goto:%d", aDestination );
 }
 
 
diff --git a/common/plotters/PDF_plotter.cpp b/common/plotters/PDF_plotter.cpp
index e738a1eb27..0a2456ea35 100644
--- a/common/plotters/PDF_plotter.cpp
+++ b/common/plotters/PDF_plotter.cpp
@@ -34,6 +34,7 @@
 #include <wx/zstream.h>
 
 #include <advanced_config.h>
+#include <eda_text.h> // for IsGotoPageHyperlink
 #include <ignore.h>
 #include <macros.h>
 #include <trigo.h>
@@ -741,8 +742,8 @@ void PDF_PLOTTER::ClosePage()
         closePdfObject();
     }*/
 
-    // Write out all "goto" hyperlinks for the page as link annotations (compatible with pdf 1.0)
-    for( const std::pair<BOX2I, wxString>& linkPair : m_urlHyperlinksInPage )
+    // Allocate all hyperlink objects for the page and calculate their position in user space coordinates
+    for( const std::pair<BOX2I, wxString>& linkPair : m_hyperlinksInPage )
     {
         const BOX2I&    box = linkPair.first;
         const wxString& url = linkPair.second;
@@ -754,12 +755,11 @@ void PDF_PLOTTER::ClosePage()
         userSpaceBox.SetOrigin( bottomLeft );
         userSpaceBox.SetEnd( topRight );
 
-        hyperlinkHandles.push_back( startPdfObject() );
+        hyperlinkHandles.push_back( allocPdfObject() );
 
-        m_urlHyperlinksHandles.insert( { hyperlinkHandles.back(), { userSpaceBox, url } } );
+        m_hyperlinkHandles.insert( { hyperlinkHandles.back(), { userSpaceBox, url } } );
     }
-    //
-    // TODO use allocPdfObject()  !!
+
     int hyperLinkArrayHandle = -1;
 
     // If we have added any annotation links, create an array containing all the objects
@@ -807,8 +807,8 @@ void PDF_PLOTTER::ClosePage()
     // Mark the page stream as idle
     m_pageStreamHandle = 0;
 
-    //
-    m_urlHyperlinksInPage.clear();
+    // Clean up
+    m_hyperlinksInPage.clear();
 }
 
 
@@ -819,8 +819,8 @@ bool PDF_PLOTTER::StartPlot()
     // First things first: the customary null object
     m_xrefTable.clear();
     m_xrefTable.push_back( 0 );
-    m_urlHyperlinksInPage.clear();
-    m_urlHyperlinksHandles.clear();
+    m_hyperlinksInPage.clear();
+    m_hyperlinkHandles.clear();
 
     /* The header (that's easy!). The second line is binary junk required
        to make the file binary from the beginning (the important thing is
@@ -895,7 +895,7 @@ bool PDF_PLOTTER::EndPlot()
     fputs( ">>\n", m_outputFile );
     closePdfObject();
 
-    for( const std::pair<int,std::pair<BOX2D, wxString>>& handlePair : m_urlHyperlinksHandles )
+    for( const std::pair<int,std::pair<BOX2D, wxString>>& handlePair : m_hyperlinkHandles )
     {
         const int& linkhandle = handlePair.first;
         const std::pair<BOX2D, wxString>& linkpair = handlePair.second;
@@ -907,10 +907,37 @@ bool PDF_PLOTTER::EndPlot()
         fprintf( m_outputFile,
                  "<< /Type /Annot\n"
                  "   /Subtype /Link\n"
-                 "   /Rect[%g %g %g %g] /Border[16 16 1]\n"
-                 "   /Dest [%d 0 R] >>\n" // /D [3 0 R /FitR �4 399 199 533]
-                 ">>\n",
-                 box.GetLeft(), box.GetBottom(), box.GetRight(), box.GetTop(), m_pageHandles[0] );
+                 "   /Rect[%g %g %g %g] /Border[16 16 1]\n",
+                 box.GetLeft(), box.GetBottom(), box.GetRight(), box.GetTop() );
+
+        int pageIdx = -1;
+
+        if( EDA_TEXT::IsGotoPageHyperlink( url, &pageIdx ) )
+        {
+            if( pageIdx <= m_pageHandles.size() && pageIdx > 0 )
+            {
+                fprintf( m_outputFile,
+                         "   /Dest [%d 0 R] >>\n"
+                         ">>\n",
+                         m_pageHandles[pageIdx - 1] );
+                //todo: do we want to support specifying zoom factor/ position? e.g. /FitR
+            }
+            else
+            {
+                // destination page is not being plotted, assign the NOP action to the link
+                fprintf( m_outputFile,
+                         "   /A << /Type /Action /S /NOP >>\n"
+                         ">>\n" );
+            }
+        }
+        else
+        {
+            fprintf( m_outputFile,
+                     "   /A << /Type /Action /S /URI /URI %s >>\n"
+                     ">>\n",
+                     encodeStringForPlotter( url ).c_str() );
+        }
+
         closePdfObject();
     }
 
@@ -1056,8 +1083,8 @@ void PDF_PLOTTER::Text( const VECTOR2I&             aPos,
 }
 
 
-void PDF_PLOTTER::HyperlinkBoxURL( const BOX2I& aBox, const wxString& aDestinationURL )
+void PDF_PLOTTER::HyperlinkBox( const BOX2I& aBox, const wxString& aDestinationURL )
 {
-    m_urlHyperlinksInPage.push_back( std::make_pair( aBox, aDestinationURL ) );
+    m_hyperlinksInPage.push_back( std::make_pair( aBox, aDestinationURL ) );
 }
 
diff --git a/eeschema/sch_text.cpp b/eeschema/sch_text.cpp
index 44ca96282f..e826981611 100644
--- a/eeschema/sch_text.cpp
+++ b/eeschema/sch_text.cpp
@@ -485,7 +485,7 @@ void SCH_TEXT::Plot( PLOTTER* aPlotter, bool aBackground ) const
     }
 
     if( HasHyperlink() )
-        aPlotter->HyperlinkBoxURL( GetBoundingBox(), GetHyperlink() );
+        aPlotter->HyperlinkBox( GetBoundingBox(), GetHyperlink() );
 }
 
 
diff --git a/eeschema/sch_textbox.cpp b/eeschema/sch_textbox.cpp
index b89f8f8d67..999da78cb4 100644
--- a/eeschema/sch_textbox.cpp
+++ b/eeschema/sch_textbox.cpp
@@ -385,7 +385,7 @@ void SCH_TEXTBOX::Plot( PLOTTER* aPlotter, bool aBackground ) const
     }
 
     if( HasHyperlink() )
-        aPlotter->HyperlinkBoxURL( GetBoundingBox(), GetHyperlink() );
+        aPlotter->HyperlinkBox( GetBoundingBox(), GetHyperlink() );
 }
 
 
diff --git a/include/eda_text.h b/include/eda_text.h
index 0bf9a3b88b..a859c83e04 100644
--- a/include/eda_text.h
+++ b/include/eda_text.h
@@ -335,6 +335,22 @@ public:
      */
     static bool ValidateHyperlink( const wxString& aURL );
 
+    /**
+     * Check if aURL is a valid "goto" hyperlink.
+     *
+     * @param aURL String to validate
+     * @param aDestinationIdx optional. pointer to populate with the page index destination
+     * @return true if aURL is a valid hyperlink
+     */
+    static bool IsGotoPageHyperlink( const wxString& aURL, int* aDestination = nullptr );
+
+    /**
+     * Generate a hyperlink string that goes to the page number specified
+     * @param aDestination Virtual page number to go to. Note that the root sheet is 1.
+     * @return A hyperlink String that goes to the page number specified
+     */
+    static wxString GotoPageHyperlinkString( const int& aDestination );
+
 protected:
     /**
      * A hyperlink to a URL or file in the system. If empty, this text object is not a hyperlink
diff --git a/include/plotters/plotter.h b/include/plotters/plotter.h
index 393e4768af..b805bd3c94 100644
--- a/include/plotters/plotter.h
+++ b/include/plotters/plotter.h
@@ -440,9 +440,9 @@ public:
      * Create a clickable hyperlink with a rectangular click area
      *
      * @aBox is the rectangular click target
-     * @aDestinationURL is the target
+     * @aDestinationURL is the target URL
      */
-    virtual void HyperlinkBoxURL( const BOX2I& aBox, const wxString& aDestinationURL )
+    virtual void HyperlinkBox( const BOX2I& aBox, const wxString& aDestinationURL )
     {
         // NOP for most plotters.
     }
diff --git a/include/plotters/plotters_pslike.h b/include/plotters/plotters_pslike.h
index 1c784bb46b..74d3d64ae9 100644
--- a/include/plotters/plotters_pslike.h
+++ b/include/plotters/plotters_pslike.h
@@ -352,7 +352,7 @@ public:
                        KIFONT::FONT*               aFont = nullptr,
                        void*                       aData = nullptr ) override;
 
-    virtual void HyperlinkBoxURL( const BOX2I& aBox, const wxString& aDestinationURL ) override;
+    virtual void HyperlinkBox( const BOX2I& aBox, const wxString& aDestinationURL ) override;
 
     /**
      * PDF images are handles as inline, not XObject streams...
@@ -417,11 +417,11 @@ protected:
     FILE* m_workFile;               ///< Temporary file to construct the stream before zipping
     std::vector<long> m_xrefTable;  ///< The PDF xref offset table
 
-    ///< List of loaded URLs in current page
-    std::vector<std::pair<BOX2I, wxString>> m_urlHyperlinksInPage;
+    ///< List of loaded hyperlinks in current page
+    std::vector<std::pair<BOX2I, wxString>> m_hyperlinksInPage;
 
     ///< Handles for all the hyperlink objects that will be deferred
-    std::map<int, std::pair<BOX2D, wxString>> m_urlHyperlinksHandles;
+    std::map<int, std::pair<BOX2D, wxString>> m_hyperlinkHandles;
 };